moleculer-db
Version:
Moleculer service to store entities in database
1,398 lines (1,196 loc) • 40.3 kB
JavaScript
"use strict";
const { ServiceBroker, Service, Context } = require("moleculer");
const { ValidationError } = require("moleculer").Errors;
const DbService = require("../../src");
//const lolex = require("lolex");
function protectReject(err) {
if (err && err.stack) {
console.error(err);
console.error(err.stack);
}
expect(err).toBe(true);
}
describe("Test DbService actions", () => {
const doc = { id : 1 };
const docs = [doc];
const adapter = {
init: jest.fn(() => Promise.resolve()),
connect: jest.fn(() => Promise.resolve()),
disconnect: jest.fn(() => Promise.resolve()),
find: jest.fn(() => Promise.resolve(docs)),
findById: jest.fn(() => Promise.resolve(doc)),
findByIds: jest.fn(() => Promise.resolve(docs)),
count: jest.fn(() => Promise.resolve(3)),
insert: jest.fn(() => Promise.resolve(doc)),
insertMany: jest.fn(() => Promise.resolve(docs)),
updateMany: jest.fn(() => Promise.resolve(docs)),
updateById: jest.fn(() => Promise.resolve(doc)),
removeMany: jest.fn(() => Promise.resolve(5)),
removeById: jest.fn(() => Promise.resolve(3)),
clear: jest.fn(() => Promise.resolve(3)),
entityToObject: jest.fn(obj => obj),
beforeSaveTransformID: jest.fn(obj => obj),
afterRetrieveTransformID: jest.fn(obj => obj)
};
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
adapter,
});
service.sanitizeParams = jest.fn((ctx, p) => p);
service.transformDocuments = jest.fn((ctx, params, docs) => Promise.resolve(docs));
it("should set default settings", () => {
expect(service.adapter).toEqual(adapter);
expect(service.settings).toEqual({
entityValidator: null,
fields: null,
excludeFields: null,
idField: "_id",
maxLimit: -1,
maxPageSize: 100,
pageSize: 10,
populates: null,
useDotNotation: false,
cacheCleanEventType: "broadcast"
});
});
it("should called the 'init' method of adapter", () => {
expect(adapter.init).toHaveBeenCalledTimes(1);
expect(adapter.init).toHaveBeenCalledWith(broker, service);
});
it("should call the 'connect' method", () => {
service.connect = jest.fn(() => Promise.resolve());
return broker.start().then(() => {
expect(service.connect).toHaveBeenCalledTimes(1);
}).catch(protectReject);
});
it("should call the 'getById' method", () => {
service.sanitizeParams.mockClear();
service.transformDocuments.mockClear();
service.getById = jest.fn(() => Promise.resolve(doc));
const p = { id: 5 };
return broker.call("store.get", p).then(() => {
expect(service.sanitizeParams).toHaveBeenCalledTimes(1);
expect(service.sanitizeParams).toHaveBeenCalledWith(expect.any(Context), p);
expect(service.getById).toHaveBeenCalledTimes(1);
expect(service.getById).toHaveBeenCalledWith(5, true);
expect(service.transformDocuments).toHaveBeenCalledTimes(1);
expect(service.transformDocuments).toHaveBeenCalledWith(expect.any(Context), p, doc);
}).catch(protectReject);
});
it("should call the 'getById' method with multi IDs, and should convert the result to object", () => {
service.transformDocuments.mockClear();
service.adapter.afterRetrieveTransformID.mockClear();
const p = { id: [5, 3, 8], mapping: true };
let docs = [
{ _id: 5, name: "John" },
{ _id: 3, name: "Walter" },
{ _id: 8, name: "Jane" }
];
service.getById = jest.fn(() => Promise.resolve(docs));
return broker.call("store.get", p).then(res => {
expect(res).toEqual({
"3": {
"_id": 3,
"name": "Walter"
},
"5": {
"_id": 5,
"name": "John"
},
"8": {
"_id": 8,
"name": "Jane"
}
});
expect(service.getById).toHaveBeenCalledTimes(1);
expect(service.getById).toHaveBeenCalledWith([5, 3, 8], true);
expect(service.transformDocuments).toHaveBeenCalledTimes(1);
expect(service.transformDocuments).toHaveBeenCalledWith(expect.any(Context), p, docs);
expect(service.adapter.afterRetrieveTransformID).toHaveBeenCalledTimes(3);
expect(service.adapter.afterRetrieveTransformID).toHaveBeenCalledWith(docs[0], "_id");
expect(service.adapter.afterRetrieveTransformID).toHaveBeenCalledWith(docs[1], "_id");
expect(service.adapter.afterRetrieveTransformID).toHaveBeenCalledWith(docs[2], "_id");
}).catch(protectReject);
});
it("should call the 'getById' method with single ID, and should convert the result to object", () => {
service.transformDocuments.mockClear();
service.adapter.afterRetrieveTransformID.mockClear();
const p = { id: 5, mapping: true };
let docs = { _id: 5, name: "John" };
service.getById = jest.fn(() => Promise.resolve(docs));
return broker.call("store.get", p).then(res => {
expect(res).toEqual({
"5": {
"_id": 5,
"name": "John"
}
});
expect(service.getById).toHaveBeenCalledTimes(1);
expect(service.getById).toHaveBeenCalledWith(5, true);
expect(service.transformDocuments).toHaveBeenCalledTimes(1);
expect(service.transformDocuments).toHaveBeenCalledWith(expect.any(Context), p, docs);
expect(service.adapter.afterRetrieveTransformID).toHaveBeenCalledTimes(1);
expect(service.adapter.afterRetrieveTransformID).toHaveBeenCalledWith(docs, "_id");
}).catch(protectReject);
});
it("should call the 'disconnect' method", () => {
service.disconnect = jest.fn();
return broker.stop().then(() => {
expect(service.disconnect).toHaveBeenCalledTimes(1);
}).catch(protectReject);
});
});
describe("Test reconnecting", () => {
const adapter = {
init: jest.fn(() => Promise.resolve()),
connect: jest.fn()
.mockImplementationOnce(() => Promise.reject("Error"))
.mockImplementationOnce(() => Promise.resolve()),
disconnect: jest.fn(() => Promise.resolve())
};
const broker = new ServiceBroker({ logger: false });
const service = broker.createService(DbService, {
name: "store",
adapter,
});
it("should connect after error", () => {
return service.schema.started.call(service).catch(protectReject).then(() => {
expect(adapter.connect).toHaveBeenCalledTimes(2);
});
});
});
describe("Test DbService methods", () => {
const doc = { id : 1 };
const docs = [doc];
const adapter = {
init: jest.fn(() => Promise.resolve()),
connect: jest.fn(() => Promise.resolve()),
disconnect: jest.fn(() => Promise.resolve()),
find: jest.fn(() => Promise.resolve(docs)),
findById: jest.fn(() => Promise.resolve(doc)),
findByIds: jest.fn(() => Promise.resolve(docs)),
count: jest.fn(() => Promise.resolve(3)),
insert: jest.fn(() => Promise.resolve(doc)),
insertMany: jest.fn(() => Promise.resolve(docs)),
updateMany: jest.fn(() => Promise.resolve(docs)),
updateById: jest.fn(() => Promise.resolve(doc)),
removeMany: jest.fn(() => Promise.resolve(5)),
removeById: jest.fn(() => Promise.resolve(3)),
clear: jest.fn(() => Promise.resolve(3)),
entityToObject: jest.fn(obj => obj)
};
const afterConnected = jest.fn();
const broker = new ServiceBroker({ logger: false, validation: false, cacher: true });
const service = broker.createService(DbService, {
name: "store",
version: 1,
adapter,
afterConnected
});
service.transformDocuments = jest.fn((ctx, params, docs) => Promise.resolve(docs));
it("should call 'afterConnected' of schema", () => {
return broker.start().delay(100).then(() => {
expect(afterConnected).toHaveBeenCalledTimes(1);
expect(adapter.connect).toHaveBeenCalledTimes(1);
}).catch(protectReject);
});
it("should call broker.broadcast to clear the cache", () => {
broker.broadcast = jest.fn();
broker.emit = jest.fn();
broker.cacher.clean = jest.fn(() => Promise.resolve());
return service.clearCache().then(() => {
expect(broker.emit).toHaveBeenCalledTimes(0);
expect(broker.broadcast).toHaveBeenCalledTimes(1);
expect(broker.broadcast).toHaveBeenCalledWith("cache.clean.v1.store");
expect(broker.cacher.clean).toHaveBeenCalledTimes(1);
expect(broker.cacher.clean).toHaveBeenCalledWith("v1.store.**");
}).catch(protectReject);
});
it("should call broker.emit to clear the cache", () => {
service.settings.cacheCleanEventType = "emit";
broker.broadcast = jest.fn();
broker.emit = jest.fn();
broker.cacher.clean = jest.fn(() => Promise.resolve());
return service.clearCache().then(() => {
expect(broker.broadcast).toHaveBeenCalledTimes(0);
expect(broker.emit).toHaveBeenCalledTimes(1);
expect(broker.emit).toHaveBeenCalledWith("cache.clean.v1.store");
expect(broker.cacher.clean).toHaveBeenCalledTimes(1);
expect(broker.cacher.clean).toHaveBeenCalledWith("v1.store.**");
}).catch(protectReject);
});
describe("Test `this.getById` method", () => {
service.encodeID = jest.fn(id => id);
service.decodeID = jest.fn(id => id);
it("call with one ID without encoding", () => {
adapter.findById.mockClear();
return service.getById(5).then(res => {
expect(res).toBe(doc);
expect(service.decodeID).toHaveBeenCalledTimes(0);
expect(adapter.findById).toHaveBeenCalledTimes(1);
expect(adapter.findById).toHaveBeenCalledWith(5);
}).catch(protectReject);
});
it("call with one ID and encoding", () => {
adapter.findById.mockClear();
return service.getById(5, true).then(res => {
expect(res).toBe(doc);
expect(service.decodeID).toHaveBeenCalledTimes(1);
expect(service.decodeID).toHaveBeenCalledWith(5);
expect(adapter.findById).toHaveBeenCalledTimes(1);
expect(adapter.findById).toHaveBeenCalledWith(5);
}).catch(protectReject);
});
it("call with multi IDs", () => {
service.encodeID.mockClear();
service.decodeID.mockClear();
adapter.findByIds.mockClear();
return service.getById([5, 3, 8], true).then(res => {
expect(res).toBe(docs);
expect(service.decodeID).toHaveBeenCalledTimes(3);
expect(service.decodeID).toHaveBeenCalledWith(5);
expect(service.decodeID).toHaveBeenCalledWith(3);
expect(service.decodeID).toHaveBeenCalledWith(8);
expect(adapter.findByIds).toHaveBeenCalledTimes(1);
expect(adapter.findByIds).toHaveBeenCalledWith([5, 3, 8]);
}).catch(protectReject);
});
});
it("should call 'disconnect' of adapter", () => {
return broker.stop().delay(100).then(() => {
expect(adapter.disconnect).toHaveBeenCalledTimes(1);
}).catch(protectReject);
});
});
describe("Test entityChanged method", () => {
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
settings: {},
beforeEntityCreate: jest.fn(() => Promise.resolve()),
beforeEntityUpdate: jest.fn(() => Promise.resolve()),
beforeEntityRemove: jest.fn(() => Promise.resolve()),
entityCreated: jest.fn(),
entityUpdated: jest.fn(),
entityRemoved: jest.fn(),
});
service.clearCache = jest.fn(() => Promise.resolve());
let ctx = {};
let doc = { id: 5 };
let docRaw = { id: 5, foo: "bar" };
it("should call `beforeEntityCreate` event", () => {
return service.beforeEntityChange("create", {}, ctx).catch(protectReject).then(() => {
expect(service.schema.beforeEntityCreate).toHaveBeenCalledTimes(1);
expect(service.schema.beforeEntityCreate).toHaveBeenCalledWith({}, ctx);
});
});
it("should call `beforeEntityUpdate` event", () => {
return service.beforeEntityChange("update", {}, ctx).catch(protectReject).then(() => {
expect(service.schema.beforeEntityUpdate).toHaveBeenCalledTimes(1);
expect(service.schema.beforeEntityUpdate).toHaveBeenCalledWith({}, ctx);
});
});
it("should call `beforeEntityRemove` event", () => {
return service.beforeEntityChange("remove",{}, ctx).catch(protectReject).then(() => {
expect(service.schema.beforeEntityRemove).toHaveBeenCalledTimes(1);
expect(service.schema.beforeEntityRemove).toHaveBeenCalledWith({}, ctx);
});
});
it("should call `entityCreated` event", () => {
return service.entityChanged("created", doc, ctx, docRaw).catch(protectReject).then(() => {
expect(service.clearCache).toHaveBeenCalledTimes(1);
expect(service.schema.entityCreated).toHaveBeenCalledTimes(1);
expect(service.schema.entityCreated).toHaveBeenCalledWith(doc, ctx, docRaw);
});
});
it("should call `entityUpdated` event", () => {
service.clearCache.mockClear();
return service.entityChanged("updated", doc, ctx, docRaw).catch(protectReject).then(() => {
expect(service.clearCache).toHaveBeenCalledTimes(1);
expect(service.schema.entityUpdated).toHaveBeenCalledTimes(1);
expect(service.schema.entityUpdated).toHaveBeenCalledWith(doc, ctx, docRaw);
});
});
it("should call `entityRemoved` event", () => {
service.clearCache.mockClear();
return service.entityChanged("removed", doc, ctx, docRaw).catch(protectReject).then(() => {
expect(service.clearCache).toHaveBeenCalledTimes(1);
expect(service.schema.entityRemoved).toHaveBeenCalledTimes(1);
expect(service.schema.entityRemoved).toHaveBeenCalledWith(doc, ctx, docRaw);
});
});
});
describe("Test sanitizeParams method", () => {
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
settings: {
maxPageSize: 50,
maxLimit: 200,
pageSize: 25
}
});
let ctx = {
action: {
name: "greeter.hello"
}
};
let ctxList = {
action: {
name: "greeter.list"
}
};
it("should not touch the params", () => {
const res = service.sanitizeParams(ctx, {});
expect(res).toEqual({});
});
it("should convert limit & offset to number", () => {
const res = service.sanitizeParams(ctx, { limit: "5", offset: "10" });
expect(res).toEqual({ limit: 5, offset: 10 });
});
it("should convert page & pageSize to number", () => {
const res = service.sanitizeParams(ctx, { page: "5", pageSize: "10" });
expect(res).toEqual({ page: 5, pageSize: 10 });
});
it("should convert sort to array", () => {
const res = service.sanitizeParams(ctx, { sort: "name,createdAt votes" });
expect(res).toEqual({ sort: ["name", "createdAt", "votes"] });
});
it("should convert fields to array", () => {
const res = service.sanitizeParams(ctx, { fields: "name votes author" });
expect(res).toEqual({ fields: ["name", "votes", "author"] });
});
it("should convert excludeFields to array", () => {
const res = service.sanitizeParams(ctx, { excludeFields: "name votes author" });
expect(res).toEqual({ excludeFields: ["name", "votes", "author"] });
});
it("should convert populate to array", () => {
const res = service.sanitizeParams(ctx, { populate: "author voters" });
expect(res).toEqual({ populate: ["author", "voters"] });
});
it("should convert searchFields to array", () => {
const res = service.sanitizeParams(ctx, { searchFields: "name votes author" });
expect(res).toEqual({ searchFields: ["name", "votes", "author"] });
});
it("should parse query to object", () => {
const res = service.sanitizeParams(ctx, { query: "{\"name\": \"moleculer\" }" });
expect(res).toEqual({ query: { name: "moleculer"} });
});
it("should fill pagination fields", () => {
const res = service.sanitizeParams(ctxList, {});
expect(res).toEqual({ limit: 25, offset: 0, page: 1, pageSize: 25});
});
it("should calc limit & offset from pagination fields", () => {
const res = service.sanitizeParams(ctxList, { page: 3, pageSize: 20 });
expect(res).toEqual({ limit: 20, offset: 40, page: 3, pageSize: 20});
});
it("should limit the pageSize", () => {
const res = service.sanitizeParams(ctxList, { page: 1, pageSize: 100 });
expect(res).toEqual({ limit: 50, offset: 0, page: 1, pageSize: 50});
});
it("should limit the limit", () => {
const res = service.sanitizeParams({ action: { name: "greeter.find" } }, { limit: 400 });
expect(res).toEqual({ limit: 200 });
});
});
const mockAdapter = {
init: jest.fn(() => Promise.resolve()),
connect: jest.fn(() => Promise.resolve()),
disconnect: jest.fn(() => Promise.resolve()),
entityToObject: jest.fn(obj => obj),
afterRetrieveTransformID: jest.fn(obj => obj)
};
describe("Test transformDocuments method", () => {
describe("Test with object", () => {
const doc = { _id : 1 };
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
adapter: mockAdapter
});
service.encodeID = jest.fn(id => id);
service.decodeID = jest.fn(id => id);
service.populateDocs = jest.fn((ctx, docs) => Promise.resolve(docs));
service.filterFields = jest.fn(docs => Promise.resolve(docs));
it("should not call anything if the docs is null", () => {
const ctx = { params: {} };
return service.transformDocuments(ctx, ctx.params, null).then(res => {
expect(res).toBe(null);
expect(mockAdapter.entityToObject).toHaveBeenCalledTimes(0);
expect(service.populateDocs).toHaveBeenCalledTimes(0);
expect(service.filterFields).toHaveBeenCalledTimes(0);
}).catch(protectReject);
});
it("should not call anything if the docs is a Number", () => {
const ctx = { params: {} };
return service.transformDocuments(ctx, ctx.params, 100).then(res => {
expect(res).toBe(100);
expect(mockAdapter.entityToObject).toHaveBeenCalledTimes(0);
expect(service.populateDocs).toHaveBeenCalledTimes(0);
expect(service.filterFields).toHaveBeenCalledTimes(0);
}).catch(protectReject);
});
it("should call 'populateDocs' & filterFields methods", () => {
service.populateDocs.mockClear();
mockAdapter.entityToObject.mockClear();
const ctx = { params: { populate: ["author"] } };
return service.transformDocuments(ctx, ctx.params, doc).then(res => {
expect(res).toBe(doc);
expect(mockAdapter.entityToObject).toHaveBeenCalledTimes(1);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(doc, ctx, ctx.params);
expect(service.encodeID).toHaveBeenCalledTimes(1);
expect(service.encodeID).toHaveBeenCalledWith(doc._id);
expect(service.populateDocs).toHaveBeenCalledTimes(1);
expect(service.populateDocs).toHaveBeenCalledWith(ctx, [doc], ["author"]);
expect(service.filterFields).toHaveBeenCalledTimes(1);
expect(service.filterFields).toHaveBeenCalledWith(doc, service.settings.fields);
}).catch(protectReject);
});
it("should not call 'populateDocs' but filterFields methods", () => {
service.filterFields.mockClear();
service.populateDocs.mockClear();
service.encodeID.mockClear();
mockAdapter.entityToObject.mockClear();
const ctx = { params: { fields: ["name"] } };
return service.transformDocuments(ctx, ctx.params, doc).then(res => {
expect(res).toBe(doc);
expect(mockAdapter.entityToObject).toHaveBeenCalledTimes(1);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(doc, ctx, ctx.params);
expect(service.encodeID).toHaveBeenCalledTimes(1);
expect(service.encodeID).toHaveBeenCalledWith(doc._id);
expect(service.populateDocs).toHaveBeenCalledTimes(0);
expect(service.filterFields).toHaveBeenCalledTimes(1);
expect(service.filterFields).toHaveBeenCalledWith(doc, ["name"]);
}).catch(protectReject);
});
});
describe("Test with array of object", () => {
const docs = [
{ _id : 2 },
{ _id : 5 }
];
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
adapter: mockAdapter
});
service.encodeID = jest.fn(id => id);
service.decodeID = jest.fn(id => id);
service.populateDocs = jest.fn((ctx, docs) => Promise.resolve(docs));
service.filterFields = jest.fn(docs => docs);
it("should call 'populateDocs' & filterFields methods", () => {
mockAdapter.entityToObject.mockClear();
const ctx = { params: { populate: ["author"] } };
return service.transformDocuments(ctx, ctx.params, docs).then(res => {
expect(res).toEqual(docs);
expect(mockAdapter.entityToObject).toHaveBeenCalledTimes(2);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(docs[0], ctx, ctx.params);
expect(mockAdapter.entityToObject).toHaveBeenCalledWith(docs[1], ctx, ctx.params);
expect(service.encodeID).toHaveBeenCalledTimes(2);
expect(service.encodeID).toHaveBeenCalledWith(docs[0]._id);
expect(service.encodeID).toHaveBeenCalledWith(docs[1]._id);
expect(service.populateDocs).toHaveBeenCalledTimes(1);
expect(service.populateDocs).toHaveBeenCalledWith(ctx, docs, ["author"]);
expect(service.filterFields).toHaveBeenCalledTimes(2);
expect(service.filterFields).toHaveBeenCalledWith(docs[0], service.settings.fields);
expect(service.filterFields).toHaveBeenCalledWith(docs[1], service.settings.fields);
}).catch(protectReject);
});
});
describe("Test excludeFields", () => {
describe("Test with object", function () {
const docs = { _id : 2, a: { b: 6, c: 7, d: { e: { f: 8 }, g: 9} } };
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
adapter: mockAdapter
});
it("should return expected - fields", () => {
const ctx = { params: { fields: ["a.c"] } };
return service.transformDocuments(ctx, ctx.params, docs).then(res => {
expect(res).toStrictEqual({a: { c: 7 }});
});
});
it("should return expected - excludeFields", () => {
const ctx = { params: { excludeFields: ["a.c"] } };
return service.transformDocuments(ctx, ctx.params, docs).then(res => {
expect(res).toStrictEqual({
"_id": 2,
"a": {
"b": 6,
"d": {
"e": {
"f": 8
},
"g": 9
}
}
});
});
});
it("should return expected - fields & excludeFields", () => {
const ctx = { params: { fields: ["a"], excludeFields: ["a.c"] } };
return service.transformDocuments(ctx, ctx.params, docs).then(res => {
expect(res).toStrictEqual({
"a": {
"b": 6,
"d": {
"e": {
"f": 8
},
"g": 9
}
}
});
});
});
it("should return expected - fields & excludeFields - deep", () => {
const ctx = { params: { fields: ["a"], excludeFields: ["a.d.e.f"] } };
return service.transformDocuments(ctx, ctx.params, docs).then(res => {
expect(res).toStrictEqual({
"a": {
"b": 6,
"c": 7,
"d": {
"e": {},
"g": 9
}
}
});
});
});
});
});
});
describe("Test authorizeFields method", () => {
/*const doc = {
id : 1,
name: "Walter",
address: {
street: "3828 Piermont Dr",
city: "Albuquerque",
state: "NM",
zip: "87112",
country: "USA"
},
email: "walter.white@heisenberg.com",
password: "H3153n83rg"
};*/
describe("Test with nested fields", () => {
const broker = new ServiceBroker({ logger: false });
const service = broker.createService(DbService, {
name: "store",
adapter: mockAdapter,
settings: {
fields: ["id", "name", "address", "bio.body", "mobile.carrier.name"]
}
});
it("should remove the email & password", () => {
const res = service.authorizeFields(["id", "name", "address", "email", "password", "otherProp"]);
expect(res).toEqual(["id", "name", "address"]);
});
it("should remove the email", () => {
const res = service.authorizeFields(["id", "name", "address.city", "address.state", "email"]);
expect(res).toEqual(["id", "name", "address.city", "address.state"]);
});
it("should remove the disabled bio fields", () => {
const res = service.authorizeFields(["id", "name", "bio.body.height", "bio.male", "bio.dob.year", "bio.body.hair.color"]);
expect(res).toEqual(["id", "name", "bio.body.height", "bio.body.hair.color"]);
});
it("should return empty", () => {
const res = service.authorizeFields(["carrier"]);
expect(res).toEqual([]);
});
});
describe("Test with enabled nested fields", () => {
const broker = new ServiceBroker({ logger: false });
const service = broker.createService(DbService, {
name: "store",
adapter: mockAdapter,
settings: {
fields: ["id", "name", "address.city", "address.state", "address.country", "bio.body.height", "bio.male", "bio.body.hair.color"]
}
});
it("should fill the nested enabled fields", () => {
let res = service.authorizeFields(["id", "name", "address"]);
expect(res).toEqual(["id", "name", "address.city", "address.state", "address.country"]);
res = service.authorizeFields(["id", "name", "bio.male", "bio.body"]);
expect(res).toEqual(["id", "name", "bio.male", "bio.body.height", "bio.body.hair.color"]);
});
});
});
describe("Test filterFields method", () => {
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store"
});
describe("Object test", () => {
const doc = {
id : 1,
name: "Walter",
address: {
city: "Albuquerque",
state: "NM",
zip: 87111
}
};
it("should not touch the doc", () => {
const res = service.filterFields(doc);
expect(res).toBe(doc);
});
it("should filter the fields", () => {
const res = service.filterFields(doc, ["name", "address"]);
expect(res).toEqual({
name: "Walter",
address: doc.address
});
});
it("should filter with nested fields", () => {
const res = service.filterFields(doc, ["name", "address.city", "address.zip"]);
expect(res).toEqual({
name: "Walter",
address: {
city: "Albuquerque",
zip: 87111
}
});
});
});
describe("Array test", () => {
describe("common case", () => {
const doc = {
id : 1,
name: "Walter",
cars: [
{id: 1, name: "BMW", model: "320i", wheels: [
{ placement: "front-left", id: 1},
{ placement: "front-right", id: 2},
{ placement: "behind-left", id: 3},
{ placement: "behind-right", id: 4},
]},
{id: 2, name: "BMW", model: "520i", wheels: [
{ placement: "front-left", id: 1},
{ placement: "front-right", id: 2},
{ placement: "behind-left", id: 3},
{ placement: "behind-right", id: 4},
]},
{id: 3, name: "AUDI", model: "Q7", wheels: [
{ placement: "front-left", id: 1, histories: []},
{ placement: "front-right", id: 2, histories: [
{date: "11/11/2011", message: "replace new 2011"}
]},
{ placement: "behind-left", id: 3, histories: []},
{ placement: "behind-right", id: 4, histories: [
{date: "12/12/2012", message: "replace new 2012"}
]},
]},
],
models: {
id: 1,
desc: "not an array",
items: [
{id: 0, desc: "0 desc", name: "0 name"},
{id: 1, desc: "1 desc", name: "1 name"},
{id: 2, desc: "2 desc", name: "2 name"},
]
}
};
it("should pass", () => {
const res = service.filterFields(doc, ["name", "cars.$.id", "cars.$.name", "cars.$.wheels.$.placement", "cars.$.wheels.$.histories.$.date", "cars.$.wheels.$.histories.$.non-existed"]);
expect(res).toEqual({
name: "Walter",
cars: [
{id: 1, name: "BMW", wheels: [
{ placement: "front-left" },
{ placement: "front-right" },
{ placement: "behind-left" },
{ placement: "behind-right" },
]},
{id: 2, name: "BMW", wheels: [
{ placement: "front-left" },
{ placement: "front-right" },
{ placement: "behind-left" },
{ placement: "behind-right" },
]},
{id: 3, name: "AUDI", wheels: [
{ placement: "front-left" },
{ placement: "front-right", histories: [{date: "11/11/2011"}] },
{ placement: "behind-left" },
{ placement: "behind-right", histories: [{date: "12/12/2012"}] },
]},
]
});
});
it("test .$. with object", () => {
const res = service.filterFields(doc, ["models.$.desc", "name"]);
expect(res).toEqual({
name: "Walter",
});
});
describe("doc.models test", function () {
it("test .$", () => {
const res = service.filterFields(doc, ["name", "models.items.$.id"]);
expect(res).toEqual({
name: "Walter",
models: {
items: [
{id: 0},
{id: 1},
{id: 2},
]
},
});
});
it("test .0", () => {
const res = service.filterFields(doc, ["name", "models.items.0.id"]);
expect(res).toEqual({
name: "Walter",
models: {
items: [
{id: 0},
]
},
});
});
it("test .1", () => {
const res = service.filterFields(doc, ["name", "models.items.1.id"]);
expect(res).toEqual({
name: "Walter",
models: {
items: [
undefined,
{id: 1},
]
},
});
});
it("test multiple array indexes", () => {
const res = service.filterFields(doc, ["name", "models.items.0.id", "models.items.1.desc", "models.items.2.name", "models.items.3.invalid"]);
expect(res).toEqual({
name: "Walter",
models: {
items: [
{ id:0 },
{ desc: "1 desc" },
{ name: "2 name"},
]
},
});
});
});
});
describe("array-index vs object key", function () {
const doc = {
a: {
2: {
b: [
{
c: [
{ d: 3, e: 4 },
{ d: 5, e: 6 },
]
}
]
}
}
};
it("object key", () => {
const res = service.filterFields(doc, ["a.2.b.$.c.$.e"]);
expect(res).toEqual({a: { 2: { b: [{c: [{e: 4}, {e: 6}]}] } }});
});
it("asked array but got object key", () => {
const res = service.filterFields(doc, ["a.$.b.$.c.2.e"]);
expect(res).toEqual({});
});
it("array index", () => {
const res = service.filterFields(doc, ["a.2.b.$.c.1.e"]);
expect(res).toEqual({a: { 2: { b: [{c: [undefined, {e: 6}]}] } }});
});
describe("multiple fields", function () {
it("should overwrite", () => {
const res = service.filterFields(doc, ["a.2.b", "a.2.b.$.c.1.e"]);
expect(res).toEqual({a: { 2: { b: doc.a["2"].b } }});
});
it("should merge", () => {
const res = service.filterFields(doc, ["a.2.b.$.c.1.d", "a.2.b.$.c.1.e"]);
expect(res).toEqual({a: { 2: { b: [{c: [undefined, {d: 5, e: 6}]}] } }});
});
it("should pass", () => {
const res = service.filterFields(doc, ["a.2.b.$.c.$.d", "a.2.b.$.c.1.e"]);
expect(res).toEqual({a: { 2: { b: [{c: [{d: 3}, {d: 5, e: 6}]}] } }});
});
});
});
describe("Object with key '$'", function () {
const doc = {
$: [
{
a: {
$: {
b: [
{c: 1, d: 2},
{c: 3, d: 4},
]
}
}
},
{
a: {
$: {
b: [
{c: 5, d: 6},
{c: 7, d: 8},
]
}
}
},
]
};
it("should handleable $", () => {
const res = service.filterFields(doc, ["$.$.a.$.b.$.c"]);
expect(res).toEqual({
$: [
{a: { $: { b: [
{ c: 1 },
{ c: 3 },
]}}},
{a: { $: { b: [
{ c: 5 },
{ c: 7 },
]}}}
]
});
});
});
});
});
describe("Test excludeFields method", () => {
const doc = {
id : 1,
name: "Walter",
address: {
city: "Albuquerque",
state: "NM",
zip: 87111
}
};
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
});
it("should not touch the doc", () => {
const res = service.excludeFields(doc);
expect(res).toBe(doc);
});
it("should exclude fields", () => {
const res = service.excludeFields(doc, ["address"]);
expect(res).toEqual({
id: 1,
name: "Walter",
});
});
it("should work with nested fields", () => {
const res = service.excludeFields(doc, ["name", "address.city", "address.zip"]);
expect(res).toEqual({
id : 1,
address: {
state: "NM",
}
});
});
});
describe("Test populateDocs method", () => {
const RATES = ["No rate", "Poor", "Acceptable", "Average", "Good", "Excellent"];
const docs = [{ id: 1, author: 3, rate: 4 }, { id: 2, author: 5, comments: [8, 3, 8], rate: 0 }, { id: 3, author: 8, rate: 5 }];
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
adapter: mockAdapter,
settings: {
populates: {
"likes.users": "users.get",
"comments": "comments.get",
"author": {
action: "users.get",
params: {
fields: "username fullName"
}
},
"rate": jest.fn(function(ids, docs) {
docs.forEach(doc => doc.rate = RATES[doc.rate]);
return this.Promise.resolve();
})
}
}
});
it("should call 'populateDocs' with rules from settings", () => {
const ctx = { params: {} };
ctx.call = jest.fn(() => Promise.resolve({
"3": {
"fullName": "Walter"
},
"5": {
"fullName": "John"
},
"8": {
"fullName": "Jane"
}
})).mockImplementationOnce(() => Promise.resolve({
"8": { id: 8, title: "Lorem" },
"3": { id: 3, title: "ipsum" }
}));
return service.populateDocs(ctx, docs, ["author", "comments", "rate"]).then(res => {
expect(ctx.call).toHaveBeenCalledTimes(2);
expect(ctx.call).toHaveBeenCalledWith("users.get", {
fields: "username fullName",
id: [3, 5, 8],
mapping: true
});
expect(ctx.call).toHaveBeenCalledWith("comments.get", {
id: [8, 3],
mapping: true
});
expect(service.settings.populates.rate).toHaveBeenCalledTimes(1);
expect(service.settings.populates.rate).toHaveBeenCalledWith([4, 5], docs, {
field: "rate",
handler: expect.any(Function)
}, ctx);
expect(res).toEqual([
{
"author": {
"fullName": "Walter"
},
"comments": undefined,
"id": 1,
"rate": "Good"
},
{
"author": {
"fullName": "John"
},
"comments": [
{
"id": 8,
"title": "Lorem"
},
{
"id": 3,
"title": "ipsum"
},
{
"id": 8,
"title": "Lorem"
}
],
"id": 2,
"rate": "No rate"
},
{
"author": {
"fullName": "Jane"
},
"comments": undefined,
"id": 3,
"rate": "Excellent"
}
]);
}).catch(protectReject);
});
it("should call 'populateDocs' with multiple doc & only author population", () => {
const ctx = { params: {} };
ctx.call = jest.fn(() => Promise.resolve({
"3": {
"name": "Walter"
},
"5": {
"name": "John"
},
"8": {
"name": "Jane"
}
}));
const docs = [
{ author: 8 },
{ author: 5 },
{ author: 8 },
{ author: 13 }
];
return service.populateDocs(ctx, docs, ["author", "voters"]).then(res => {
expect(res).toEqual([
{ author: { name: "Jane" } },
{ author: { name: "John" } },
{ author: { name: "Jane" } },
{ author: undefined },
]);
}).catch(protectReject);
});
it("should call 'populateDocs' with single doc & only author population", () => {
const ctx = { params: {} };
ctx.call = jest.fn(() => Promise.resolve({
"3": {
"name": "Walter"
},
"5": {
"name": "John"
},
"8": {
"name": "Jane"
}
}));
const doc = { author: 8 };
return service.populateDocs(ctx, doc, ["author", "voters"]).then(res => {
expect(res).toEqual({ author: { name: "Jane" } });
}).catch(protectReject);
});
it("should call 'populateDocs' with single doc & only likes.users population", () => {
const ctx = { params: {} };
ctx.call = jest.fn(() => Promise.resolve({
"3": {
"name": "Walter"
},
"5": {
"name": "John"
},
"8": {
"name": "Jane"
}
}));
const doc = { id: 4, likes: { users: [8, 3], shared: 4 } };
return service.populateDocs(ctx, doc, ["likes.users"]).then(res => {
expect(res).toEqual({
id: 4,
likes: {
users: [{ name: "Jane" }, { name: "Walter" }],
shared: 4,
},
});
}).catch(protectReject);
});
it("should return docs if no populate list", () => {
const docs = [];
const ctx = { params: {} };
return service.populateDocs(ctx, docs).then(res => {
expect(res).toBe(docs);
}).catch(protectReject);
});
it("should return docs if docs is not array or object", () => {
const docs = 5;
const ctx = { params: {} };
return service.populateDocs(ctx, docs, ["author"]).then(res => {
expect(res).toBe(docs);
}).catch(protectReject);
});
});
describe("Test validateEntity method", () => {
describe("Test with custom validator function", () => {
const validator = jest.fn();
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
adapter: mockAdapter,
settings: {
entityValidator: validator
}
});
it("should call 'entityValidator'", () => {
let entity = { param1: "value1", param2: "value2" };
return service.validateEntity(entity).catch(protectReject).then(() => {
expect(validator).toHaveBeenCalledTimes(1);
expect(validator).toHaveBeenCalledWith(entity);
expect(validator.mock.instances[0]).toBeInstanceOf(Service);
});
});
it("should call 'entityValidator' multiple times", () => {
validator.mockClear();
let entities = [{}, { param: "value" }];
return service.validateEntity(entities).catch(protectReject).then(() => {
expect(validator).toHaveBeenCalledTimes(2);
expect(validator).toHaveBeenCalledWith(entities[0]);
expect(validator).toHaveBeenCalledWith(entities[1]);
expect(validator.mock.instances[0]).toBeInstanceOf(Service);
expect(validator.mock.instances[1]).toBeInstanceOf(Service);
});
});
});
describe("Test with built-in validator function", () => {
const broker = new ServiceBroker({ logger: false, validation: {
options: { useNewCustomCheckerFunction: true } // Enable async validations
} });
const service = broker.createService(DbService, {
name: "store",
adapter: mockAdapter,
settings: {
entityValidator: {
id: "number",
name: "string"
}
}
});
// Service with async validation
const otherService = broker.createService(DbService, {
name: "shop",
adapter: mockAdapter,
settings: {
entityValidator: {
$$async: true,
id: "number",
name: { type: "string", custom: async (value) => {
return await new Promise(resolve => { setTimeout(() => {
resolve(value);
}, 10); });
} }
}
}
});
it("should call validator with correct entity", () => {
let entity = { id: 5, name: "John" };
return service.validateEntity(entity).catch(protectReject).then(res => {
expect(res).toBe(entity);
});
});
it("should call validator with incorrect entity", () => {
let entities = [{ id: 5, name: "John" }, { name: "Jane" }];
return service.validateEntity(entities).then(protectReject).catch(err => {
expect(err).toBeDefined();
expect(err).toBeInstanceOf(ValidationError);
expect(err.code).toBe(422);
expect(err.message).toBe("Entity validation error!");
expect(err.data[0].type).toBe("required");
expect(err.data[0].field).toBe("id");
});
});
// Async validator
it("should call async validator with correct entity", () => {
let entity = { id: 5, name: "Mario" };
return otherService.validateEntity(entity).catch(protectReject).then(res => {
expect(res).toBe(entity);
});
});
it("should call async validator with incorrect entity", () => {
let entity = { name: "Luigi" };
return otherService.validateEntity(entity).then(protectReject).catch(err => {
expect(err).toBeDefined();
expect(err).toBeInstanceOf(ValidationError);
expect(err.code).toBe(422);
expect(err.message).toBe("Entity validation error!");
expect(err.data[0].type).toBe("required");
expect(err.data[0].field).toBe("id");
});
});
});
});
describe("Test encodeID/decodeID method", () => {
const broker = new ServiceBroker({ logger: false, validation: false });
const service = broker.createService(DbService, {
name: "store",
adapter: mockAdapter,
settings: {}
});
it("should return with the same ID", () => {
expect(service.encodeID(5)).toBe(5);
});
it("should return with the same ID", () => {
expect(service.decodeID(5)).toBe(5);
});
});