moleculer
Version:
Fast & powerful microservices framework for Node.JS
815 lines (709 loc) • 18 kB
JavaScript
const Promise = require("bluebird");
const ServiceBroker = require("../../src/service-broker");
const MemoryCacher = require("../../src/cachers/memory");
const FakeTransporter = require("../../src/transporters/fake");
const Context = require("../../src/context");
const utils = require("../../src/utils");
describe("Test load services", () => {
let broker = new ServiceBroker();
it("should create service from schema", () => {
let handler = jest.fn();
broker.createService({
name: "mailer",
version: 2,
actions: {
send: handler
}
});
expect(broker.hasAction("v2.mailer.send")).toBe(true);
broker.call("v2.mailer.send").then(() => {
expect(handler).toHaveBeenCalledTimes(1);
});
});
it("should load all services", () => {
let count = broker.loadServices("./test/services");
expect(count).toBe(3);
expect(broker.hasAction("math.add")).toBe(true);
expect(broker.hasAction("users.get")).toBe(true);
expect(broker.getService("posts").name).toBe("posts");
});
});
describe("Test broker.registerInternalActions", () => {
let broker = new ServiceBroker({
statistics: true,
internalActions: true,
nodeID: "server-1",
transporter: new FakeTransporter()
});
it("should register $node.stats internal action", () => {
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);
expect(broker.hasAction("$node.stats")).toBe(true);
});
broker.loadService("./test/services/math.service");
broker.loadService("./test/services/post.service");
it("should return list of actions", () => {
return broker.call("$node.actions").then(res => {
expect(res).toEqual([
{
"action": {
"cache": false,
"name": "$node.list"
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "$node.list"
},
{
"action": {
"cache": false,
"name": "$node.services"
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "$node.services"
},
{
"action": {
"cache": false,
"name": "$node.actions"
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "$node.actions"
},
{
"action": {
"cache": false,
"name": "$node.health"
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "$node.health"
},
{
"action": {
"cache": false,
"name": "$node.stats"
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "$node.stats"
},
{
"action": {
"cache": false,
"name": "math.add",
"version": undefined
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "math.add"
},
{
"action": {
"cache": false,
"name": "math.sub",
"version": undefined
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "math.sub"
},
{
"action": {
"cache": false,
"name": "math.mult",
"params": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"version": undefined
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "math.mult"
},
{
"action": {
"cache": false,
"name": "math.div",
"version": undefined
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "math.div"
},
{
"action": {
"cache": true,
"name": "posts.find",
"version": undefined
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "posts.find"
},
{
"action": {
"cache": false,
"name": "posts.delayed",
"version": undefined
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "posts.delayed"
},
{
"action": {
"cache": {
"keys": [
"id"
]
},
"name": "posts.get",
"version": undefined
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "posts.get"
},
{
"action": {
"cache": false,
"name": "posts.author",
"version": undefined
},
"available": true,
"count": 1,
"hasLocal": true,
"name": "posts.author"
}
]);
});
});
it("should return list of services", () => {
let service = {
name: "math",
settings: {}
};
broker.registerRemoteService("node-3", service);
broker.registerAction("node-3", {
name: "math.pow",
cache: true,
service
});
return broker.call("$node.services", { withActions: true }).then(res => {
expect(res).toEqual([{
"actions": {
"$node.actions": {
"cache": false,
"name": "$node.actions"
},
"$node.health": {
"cache": false,
"name": "$node.health"
},
"$node.list": {
"cache": false,
"name": "$node.list"
},
"$node.services": {
"cache": false,
"name": "$node.services"
},
"$node.stats": {
"cache": false,
"name": "$node.stats"
}
},
"name": "$node",
"nodes": [null],
"settings": {},
"version": undefined
}, {
"actions": {
"math.add": {
"cache": false,
"name": "math.add",
"version": undefined
},
"math.div": {
"cache": false,
"name": "math.div",
"version": undefined
},
"math.mult": {
"cache": false,
"name": "math.mult",
"params": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"version": undefined
},
"math.pow": {
"cache": true,
"name": "math.pow",
},
"math.sub": {
"cache": false,
"name": "math.sub",
"version": undefined
}
},
"name": "math",
"nodes": [null, "node-3"],
"settings": {},
"version": undefined
}, {
"actions": {
"posts.author": {
"cache": false,
"name": "posts.author",
"version": undefined
},
"posts.delayed": {
"cache": false,
"name": "posts.delayed",
"version": undefined
},
"posts.find": {
"cache": true,
"name": "posts.find",
"version": undefined
},
"posts.get": {
"cache": {
"keys": ["id"]
},
"name": "posts.get",
"version": undefined
}
},
"name": "posts",
"nodes": [null],
"settings": {},
"version": undefined
}]);
});
});
it("should return health of node", () => {
return broker.call("$node.health").then(res => {
expect(res).toBeDefined();
expect(res.cpu).toBeDefined();
expect(res.mem).toBeDefined();
expect(res.net).toBeDefined();
expect(res.os).toBeDefined();
expect(res.process).toBeDefined();
expect(res.time).toBeDefined();
});
});
it("should return statistics of node", () => {
return broker.call("$node.stats").then(res => {
expect(res).toBeDefined();
expect(res.requests).toBeDefined();
});
});
it("should return list of remote nodes", () => {
let info = {
nodeID: "server-2",
services: []
};
broker.transit.processNodeInfo(info.nodeID, info);
return broker.call("$node.list").then(res => {
expect(res).toBeInstanceOf(Array);
expect(res.length).toBe(2);
expect(res[0].id).toBeNull();
expect(res[0].services.length).toBe(3);
expect(res[1]).toEqual({"services": [], "available": true, "id": "server-2", "lastHeartbeatTime": jasmine.any(Number), "nodeID": "server-2"});
});
});
});
describe("Test on/once/off event emitter", () => {
let broker = new ServiceBroker();
let handler = jest.fn();
it("register event handler", () => {
broker.on("test.event.**", handler);
broker.emitLocal("test");
expect(handler).toHaveBeenCalledTimes(0);
let p = { a: 5 };
broker.emitLocal("test.event", p);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler).toHaveBeenCalledWith(p, undefined);
broker.emitLocal("test.event.demo", "data");
expect(handler).toHaveBeenCalledTimes(2);
expect(handler).toHaveBeenCalledWith("data", undefined);
});
it("unregister event handler", () => {
handler.mockClear();
broker.off("test.event.**", handler);
broker.emitLocal("test");
broker.emitLocal("test.event");
broker.emitLocal("test.event.demo");
expect(handler).toHaveBeenCalledTimes(0);
});
it("register once event handler", () => {
handler.mockClear();
broker.once("request", handler);
broker.emitLocal("request");
expect(handler).toHaveBeenCalledTimes(1);
broker.emitLocal("request");
expect(handler).toHaveBeenCalledTimes(1);
});
});
describe("Test local call", () => {
let broker = new ServiceBroker({ metrics: true });
let actionHandler = jest.fn(ctx => ctx);
broker.createService({
name: "posts",
actions: {
find: actionHandler
}
});
it("should return context & call the action handler", () => {
return broker.call("posts.find").then(ctx => {
expect(ctx).toBeDefined();
expect(ctx.broker).toBe(broker);
expect(ctx.action.name).toBe("posts.find");
expect(ctx.nodeID).toBeNull();
expect(ctx.params).toBeDefined();
expect(actionHandler).toHaveBeenCalledTimes(1);
expect(actionHandler).toHaveBeenCalledWith(ctx);
});
});
it("should set params to context", () => {
let params = { a: 1 };
return broker.call("posts.find", params).then(ctx => {
expect(ctx.params).toEqual({ a: 1});
});
});
it("should create a sub context of parent context", () => {
let parentCtx = new Context();
parentCtx.params = {
a: 5,
b: 2
};
parentCtx.meta = {
user: "John",
roles: ["user"]
};
let params = { a: 1 };
let meta = {
user: "Jane",
roles: ["admin"]
};
parentCtx.metrics = true;
return broker.call("posts.find", params, { parentCtx, meta }).then(ctx => {
expect(ctx.params).toBe(params);
expect(ctx.meta).toEqual({ user: "Jane", roles: ["admin"] });
expect(ctx.level).toBe(2);
expect(ctx.parentID).toBe(parentCtx.id);
});
});
});
describe("Test versioned action registration", () => {
let broker = new ServiceBroker();
let findV1 = jest.fn(ctx => ctx);
let findV2 = jest.fn(ctx => ctx);
broker.createService({
name: "posts",
version: 1,
actions: {
find: findV1
}
});
broker.createService({
name: "posts",
version: 2,
actions: {
find: findV2
}
});
it("should return with the correct action", () => {
expect(broker.hasAction("v1.posts.find")).toBe(true);
expect(broker.hasAction("v2.posts.find")).toBe(true);
expect(broker.hasAction("v3.posts.find")).toBe(false);
});
it("should call the v1 handler", () => {
return broker.call("v1.posts.find").then(ctx => {
expect(findV1).toHaveBeenCalledTimes(1);
});
});
it("should call the v2 handler", () => {
return broker.call("v2.posts.find").then(ctx => {
expect(findV2).toHaveBeenCalledTimes(1);
});
});
});
describe("Test middleware system", () => {
describe("Test with sync & async middlewares", () => {
let flow = [];
let mw1Sync = handler => {
return ctx => {
flow.push("B1");
return handler(ctx).then(res => {
flow.push("A1");
return res;
});
};
};
let mw2Async = handler => {
return ctx => {
flow.push("B2");
return new Promise(resolve => {
setTimeout(() => {
flow.push("B2P");
resolve();
}, 10);
}).then(() => {
return handler(ctx);
}).then(res => {
flow.push("A2");
return res;
});
};
};
let broker = new ServiceBroker({ validation: false });
broker.use(mw2Async);
broker.use(mw1Sync);
let master = jest.fn(() => {
flow.push("MASTER");
return { user: "icebob" };
});
broker.createService({
name: "test",
actions: {
foo: master
}
});
it("should register plugins", () => {
expect(broker.middlewares.length).toBe(2);
});
it("should call all middlewares functions & master", () => {
let p = broker.call("test.foo");
expect(utils.isPromise(p)).toBe(true);
return p.then(res => {
expect(res).toEqual({ user: "icebob" });
expect(master).toHaveBeenCalledTimes(1);
expect(flow.join("-")).toBe("B1-B2-B2P-MASTER-A2-A1");
});
});
});
describe("Test with SYNC break", () => {
let flow = [];
let mw1 = handler => {
return ctx => {
flow.push("B1");
return handler(ctx).then(res => {
flow.push("A1");
return res;
});
};
};
let mw2 = handler => {
return ctx => {
flow.push("B2");
// Don't call handler, break the chain
return Promise.resolve({ user: "bobcsi" });
};
};
let mw3 = handler => {
return ctx => {
flow.push("B3");
return handler(ctx).then(res => {
flow.push("A3");
return res;
});
};
};
let master = jest.fn(() => {
flow.push("MASTER");
return { user: "icebob" };
});
let broker = new ServiceBroker({ validation: false });
broker.use(mw3, mw2, mw1);
broker.createService({
name: "test",
actions: {
foo: master
}
});
it("should register plugins", () => {
expect(broker.middlewares.length).toBe(3);
});
it("should call only mw1 & mw2 middlewares functions", () => {
let p = broker.call("test.foo");
expect(utils.isPromise(p)).toBe(true);
return p.then(res => {
expect(res).toEqual({ user: "bobcsi" });
expect(master).toHaveBeenCalledTimes(0);
expect(flow.join("-")).toBe("B1-B2-A1");
});
});
});
describe("Test middleware system with ASYNC break", () => {
let flow = [];
let mw1 = handler => {
return ctx => {
flow.push("B1");
return handler(ctx).then(res => {
flow.push("A1");
return res;
});
};
};
let mw2 = handler => {
return ctx => {
flow.push("B2");
return new Promise(resolve => {
setTimeout(() => {
flow.push("B2P");
resolve({ user: "bobcsi" });
}, 10);
});
};
};
let mw3 = handler => {
return ctx => {
flow.push("B3");
return handler(ctx).then(res => {
flow.push("A3");
return res;
});
};
};
let master = jest.fn(() => {
flow.push("MASTER");
return { user: "icebob" };
});
let broker = new ServiceBroker({ validation: false });
broker.use(mw3, mw2, mw1);
broker.createService({
name: "test",
actions: {
foo: master
}
});
it("should register plugins", () => {
expect(broker.middlewares.length).toBe(3);
});
it("should call only mw1 & mw2 middlewares functions", () => {
let p = broker.call("test.foo");
expect(utils.isPromise(p)).toBe(true);
return p.then(res => {
expect(res).toEqual({ user: "bobcsi" });
expect(master).toHaveBeenCalledTimes(0);
expect(flow.join("-")).toBe("B1-B2-B2P-A1");
});
});
});
describe("Test with Error", () => {
let flow = [];
let mw1 = handler => {
return ctx => {
flow.push("B1");
return handler(ctx).then(res => {
flow.push("A1");
return res;
});
};
};
let mw2 = handler => {
return ctx => {
flow.push("B2");
return Promise.reject(new Error("Something happened in mw2"));
};
};
let master = jest.fn(() => {
return new Promise(resolve => {
flow.push("MASTER");
resolve({ user: "icebob" });
});
});
let broker = new ServiceBroker({ validation: false });
broker.use(mw2, mw1);
broker.createService({
name: "test",
actions: {
foo: master
}
});
it("should call only mw1 & mw2 middlewares functions", () => {
let p = broker.call("test.foo");
expect(utils.isPromise(p)).toBe(true);
return p.catch(err => {
expect(err.message).toEqual("Something happened in mw2");
expect(flow.join("-")).toBe("B1-B2");
});
});
});
});
describe("Test cachers", () => {
let broker = new ServiceBroker({
cacher: new MemoryCacher()
});
let handler = jest.fn(() => "Action result");
broker.createService({
name: "user",
actions: {
get: {
cache: true,
handler
},
save(ctx) {
ctx.emit("cache.clean");
}
}
});
it("should call action handler because the cache is empty", () => {
return broker.call("user.get").then(res => {
expect(res).toBe("Action result");
expect(handler).toHaveBeenCalledTimes(1);
});
});
it("should NOT call action handler because the cache is loaded", () => {
handler.mockClear();
return broker.call("user.get").then(res => {
expect(res).toBe("Action result");
expect(handler).toHaveBeenCalledTimes(0);
});
});
it("clear the cache with `save` action", () => {
handler.mockClear();
return broker.call("user.save").then(() => {
expect(handler).toHaveBeenCalledTimes(0);
});
});
it("should NOT call action handler because the cache is loaded", () => {
handler.mockClear();
return broker.call("user.get").then(res => {
expect(res).toBe("Action result");
expect(handler).toHaveBeenCalledTimes(1);
});
});
});
describe.skip("Test validation", () => {
});