DataModule.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. // @ts-nocheck
  2. import chai from "chai";
  3. import sinon from "sinon";
  4. import sinonChai from "sinon-chai";
  5. import chaiAsPromised from "chai-as-promised";
  6. import { ObjectId } from "mongodb";
  7. import JobContext from "../JobContext";
  8. import JobQueue from "../JobQueue";
  9. import LogBook from "../LogBook";
  10. import ModuleManager from "../ModuleManager";
  11. import DataModule from "./DataModule";
  12. chai.should();
  13. chai.use(sinonChai);
  14. chai.use(chaiAsPromised);
  15. describe("Data Module", function () {
  16. const moduleManager = Object.getPrototypeOf(
  17. sinon.createStubInstance(ModuleManager)
  18. );
  19. moduleManager.logBook = sinon.createStubInstance(LogBook);
  20. moduleManager.jobQueue = sinon.createStubInstance(JobQueue);
  21. const dataModule = new DataModule(moduleManager);
  22. const jobContext = sinon.createStubInstance(JobContext);
  23. const testData = { abc: [] };
  24. before(async function () {
  25. await dataModule.startup();
  26. dataModule.redisClient = sinon.spy(dataModule.redisClient);
  27. });
  28. beforeEach(async function () {
  29. testData.abc = await Promise.all(
  30. Array.from({ length: 10 }).map(async () => {
  31. const doc = {
  32. name: `Test${Math.round(Math.random() * 1000)}`,
  33. autofill: {
  34. enabled: !!Math.round(Math.random())
  35. },
  36. someNumbers: Array.from({
  37. length: Math.max(1, Math.round(Math.random() * 50))
  38. }).map(() => Math.round(Math.random() * 10000)),
  39. songs: Array.from({
  40. length: Math.max(1, Math.round(Math.random() * 10))
  41. }).map(() => ({
  42. _id: new ObjectId()
  43. })),
  44. restrictedName: `RestrictedTest${Math.round(
  45. Math.random() * 1000
  46. )}`,
  47. createdAt: new Date(),
  48. updatedAt: new Date(),
  49. testData: true
  50. };
  51. const res =
  52. await dataModule.collections?.abc.collection.insertOne({
  53. ...doc,
  54. testData: true
  55. });
  56. return { _id: res.insertedId, ...doc };
  57. })
  58. );
  59. });
  60. it("module loaded and started", function () {
  61. moduleManager.logBook.log.should.have.been.called;
  62. dataModule.getName().should.equal("data");
  63. dataModule.getStatus().should.equal("STARTED");
  64. });
  65. describe("find job", function () {
  66. // Run cache test twice to validate mongo and redis sourced data
  67. [false, true, true].forEach(useCache => {
  68. const useCacheString = `${useCache ? "with" : "without"} cache`;
  69. it(`filter by one _id string ${useCacheString}`, async function () {
  70. const [document] = testData.abc;
  71. const find = await dataModule.find(jobContext, {
  72. collection: "abc",
  73. filter: { _id: document._id },
  74. limit: 1,
  75. useCache
  76. });
  77. find.should.deep.equal({
  78. _id: document._id,
  79. name: document.name,
  80. autofill: {
  81. enabled: document.autofill.enabled
  82. },
  83. someNumbers: document.someNumbers,
  84. songs: document.songs,
  85. createdAt: document.createdAt,
  86. updatedAt: document.updatedAt
  87. });
  88. if (useCache) {
  89. dataModule.redisClient?.GET.should.have.been.called;
  90. }
  91. });
  92. // it(`filter by name string ${useCacheString}`, async function () {
  93. // const [document] = testData.abc;
  94. // const find = await dataModule.find(jobContext, {
  95. // collection: "abc",
  96. // filter: { restrictedName: document.restrictedName },
  97. // limit: 1,
  98. // useCache
  99. // });
  100. // find.should.be.an("object");
  101. // find._id.should.deep.equal(document._id);
  102. // find.should.have.keys([
  103. // "_id",
  104. // "createdAt",
  105. // "updatedAt",
  106. // "name",
  107. // "autofill",
  108. // "someNumbers",
  109. // "songs"
  110. // ]);
  111. // find.should.not.have.keys(["restrictedName"]);
  112. // // RestrictedName is restricted, so it won't be returned and the query should not be cached
  113. // find.should.not.have.keys(["name"]);
  114. // dataModule.redisClient?.GET.should.not.have.been.called;
  115. // dataModule.redisClient?.SET.should.not.have.been.called;
  116. // });
  117. });
  118. it(`filter by normal array item`, async function () {
  119. const [document] = testData.abc;
  120. const resultDocument = await dataModule.find(jobContext, {
  121. collection: "abc",
  122. filter: { someNumbers: document.someNumbers[0] },
  123. limit: 1,
  124. useCache: false
  125. });
  126. resultDocument.should.be.an("object");
  127. resultDocument._id.should.deep.equal(document._id);
  128. });
  129. it(`filter by normal array item that doesn't exist`, async function () {
  130. const resultDocument = dataModule.find(jobContext, {
  131. collection: "abc",
  132. filter: { someNumbers: -1 },
  133. limit: 1,
  134. useCache: false
  135. });
  136. await resultDocument.should.eventually.be.null;
  137. });
  138. it(`filter by schema array item`, async function () {
  139. const [document] = testData.abc;
  140. const resultDocument = await dataModule.find(jobContext, {
  141. collection: "abc",
  142. filter: { songs: { _id: document.songs[0]._id } },
  143. limit: 1,
  144. useCache: false
  145. });
  146. resultDocument.should.be.an("object");
  147. resultDocument._id.should.deep.equal(document._id);
  148. });
  149. it(`filter by schema array item, invalid`, async function () {
  150. const jobPromise = dataModule.find(jobContext, {
  151. collection: "abc",
  152. filter: { songs: { randomProperty: "Value" } },
  153. limit: 1,
  154. useCache: false
  155. });
  156. await jobPromise.should.eventually.be.rejectedWith(
  157. `Key "randomProperty" does not exist in the schema.`
  158. );
  159. });
  160. it(`filter by schema array item with dot notation`, async function () {
  161. const [document] = testData.abc;
  162. const resultDocument = await dataModule.find(jobContext, {
  163. collection: "abc",
  164. filter: { "songs._id": document.songs[0]._id },
  165. limit: 1,
  166. useCache: false
  167. });
  168. resultDocument.should.be.an("object");
  169. resultDocument._id.should.deep.equal(document._id);
  170. });
  171. it(`filter by schema array item with dot notation, invalid`, async function () {
  172. const jobPromise = dataModule.find(jobContext, {
  173. collection: "abc",
  174. filter: { "songs.randomProperty": "Value" },
  175. limit: 1,
  176. useCache: false
  177. });
  178. await jobPromise.should.eventually.be.rejectedWith(
  179. `Key "randomProperty" does not exist in the schema.`
  180. );
  181. });
  182. describe("filter $in operator by type", function () {
  183. Object.entries({
  184. objectId: ["_id", new ObjectId()],
  185. string: ["name", "RandomName"],
  186. number: ["someNumbers", -1],
  187. date: ["createdAt", new Date()]
  188. }).forEach(([type, [attribute, invalidValue]]) => {
  189. it(`${type}, where document exists`, async function () {
  190. const [document] = testData.abc;
  191. const filter = {};
  192. filter[attribute] = {
  193. $in: [
  194. Array.isArray(document[attribute])
  195. ? document[attribute][0]
  196. : document[attribute],
  197. invalidValue
  198. ]
  199. };
  200. const resultDocument = await dataModule.find(jobContext, {
  201. collection: "abc",
  202. filter,
  203. limit: 1,
  204. useCache: false
  205. });
  206. resultDocument.should.deep.equal({
  207. _id: document._id,
  208. name: document.name,
  209. autofill: {
  210. enabled: document.autofill.enabled
  211. },
  212. someNumbers: document.someNumbers,
  213. songs: document.songs,
  214. createdAt: document.createdAt,
  215. updatedAt: document.updatedAt
  216. });
  217. });
  218. it(`${type}, where document doesnt exist`, async function () {
  219. const filter = {};
  220. filter[attribute] = { $in: [invalidValue, invalidValue] };
  221. const jobPromise = dataModule.find(jobContext, {
  222. collection: "abc",
  223. filter,
  224. limit: 1,
  225. useCache: false
  226. });
  227. await jobPromise.should.eventually.be.null;
  228. });
  229. });
  230. });
  231. it(`find should not have restricted properties`, async function () {
  232. const [document] = testData.abc;
  233. const resultDocument = await dataModule.find(jobContext, {
  234. collection: "abc",
  235. filter: { _id: document._id },
  236. limit: 1,
  237. useCache: false
  238. });
  239. resultDocument.should.be.an("object");
  240. resultDocument._id.should.deep.equal(document._id);
  241. resultDocument.should.have.all.keys([
  242. "_id",
  243. "createdAt",
  244. "updatedAt",
  245. "name",
  246. "autofill",
  247. "someNumbers",
  248. "songs"
  249. ]);
  250. resultDocument.should.not.have.any.keys(["restrictedName"]);
  251. });
  252. it(`find should have all restricted properties`, async function () {
  253. const [document] = testData.abc;
  254. const resultDocument = await dataModule.find(jobContext, {
  255. collection: "abc",
  256. filter: { _id: document._id },
  257. allowedRestricted: true,
  258. limit: 1,
  259. useCache: false
  260. });
  261. resultDocument.should.be.an("object");
  262. resultDocument._id.should.deep.equal(document._id);
  263. resultDocument.should.have.all.keys([
  264. "_id",
  265. "createdAt",
  266. "updatedAt",
  267. "name",
  268. "autofill",
  269. "someNumbers",
  270. "songs",
  271. "restrictedName"
  272. ]);
  273. });
  274. it(`find should have a specific restricted property`, async function () {
  275. const [document] = testData.abc;
  276. const resultDocument = await dataModule.find(jobContext, {
  277. collection: "abc",
  278. filter: { _id: document._id },
  279. allowedRestricted: ["restrictedName"],
  280. limit: 1,
  281. useCache: false
  282. });
  283. resultDocument.should.be.an("object");
  284. resultDocument._id.should.deep.equal(document._id);
  285. resultDocument.should.have.all.keys([
  286. "_id",
  287. "createdAt",
  288. "updatedAt",
  289. "name",
  290. "autofill",
  291. "someNumbers",
  292. "songs",
  293. "restrictedName"
  294. ]);
  295. });
  296. });
  297. describe("normalize projection", function () {
  298. const dataModuleProjection = Object.getPrototypeOf(dataModule);
  299. it(`basics`, function () {
  300. dataModuleProjection.normalizeProjection.should.be.a("function");
  301. });
  302. it(`empty object/array projection`, function () {
  303. const expectedResult = { projection: [], mode: "includeAllBut" };
  304. const resultWithArray = dataModuleProjection.normalizeProjection(
  305. []
  306. );
  307. const resultWithObject = dataModuleProjection.normalizeProjection(
  308. {}
  309. );
  310. resultWithArray.should.deep.equal(expectedResult);
  311. resultWithObject.should.deep.equal(expectedResult);
  312. });
  313. it(`null/undefined projection`, function () {
  314. const expectedResult = { projection: [], mode: "includeAllBut" };
  315. const resultWithNull =
  316. dataModuleProjection.normalizeProjection(null);
  317. const resultWithUndefined =
  318. dataModuleProjection.normalizeProjection(undefined);
  319. const resultWithNothing =
  320. dataModuleProjection.normalizeProjection();
  321. resultWithNull.should.deep.equal(expectedResult);
  322. resultWithUndefined.should.deep.equal(expectedResult);
  323. resultWithNothing.should.deep.equal(expectedResult);
  324. });
  325. it(`simple exclude projection`, function () {
  326. const expectedResult = {
  327. projection: [["name", false]],
  328. mode: "includeAllBut"
  329. };
  330. const resultWithBoolean = dataModuleProjection.normalizeProjection({
  331. name: false
  332. });
  333. const resultWithNumber = dataModuleProjection.normalizeProjection({
  334. name: 0
  335. });
  336. resultWithBoolean.should.deep.equal(expectedResult);
  337. resultWithNumber.should.deep.equal(expectedResult);
  338. });
  339. it(`simple include projection`, function () {
  340. const expectedResult = {
  341. projection: [["name", true]],
  342. mode: "excludeAllBut"
  343. };
  344. const resultWithObject = dataModuleProjection.normalizeProjection({
  345. name: true
  346. });
  347. const resultWithArray = dataModuleProjection.normalizeProjection([
  348. "name"
  349. ]);
  350. resultWithObject.should.deep.equal(expectedResult);
  351. resultWithArray.should.deep.equal(expectedResult);
  352. });
  353. it(`simple include/exclude projection`, function () {
  354. const expectedResult = {
  355. projection: [
  356. ["color", false],
  357. ["name", true]
  358. ],
  359. mode: "excludeAllBut"
  360. };
  361. const result = dataModuleProjection.normalizeProjection({
  362. color: false,
  363. name: true
  364. });
  365. result.should.deep.equal(expectedResult);
  366. });
  367. it(`simple nested include projection`, function () {
  368. const expectedResult = {
  369. projection: [["location.city", true]],
  370. mode: "excludeAllBut"
  371. };
  372. const resultWithObject = dataModuleProjection.normalizeProjection({
  373. location: {
  374. city: true
  375. }
  376. });
  377. const resultWithArray = dataModuleProjection.normalizeProjection([
  378. "location.city"
  379. ]);
  380. resultWithObject.should.deep.equal(expectedResult);
  381. resultWithArray.should.deep.equal(expectedResult);
  382. });
  383. it(`simple nested exclude projection`, function () {
  384. const expectedResult = {
  385. projection: [["location.city", false]],
  386. mode: "includeAllBut"
  387. };
  388. const result = dataModuleProjection.normalizeProjection({
  389. location: {
  390. city: false
  391. }
  392. });
  393. result.should.deep.equal(expectedResult);
  394. });
  395. it(`path collision`, function () {
  396. (() =>
  397. dataModuleProjection.normalizeProjection({
  398. location: {
  399. city: false
  400. },
  401. "location.city": true
  402. })).should.throw("Path collision, non-unique key");
  403. });
  404. it(`path collision 2`, function () {
  405. (() =>
  406. dataModuleProjection.normalizeProjection({
  407. location: {
  408. city: {
  409. extra: false
  410. }
  411. },
  412. "location.city": true
  413. })).should.throw(
  414. "Path collision! location.city.extra collides with location.city"
  415. );
  416. });
  417. // TODO add more test cases
  418. });
  419. afterEach(async function () {
  420. sinon.reset();
  421. await dataModule.collections?.abc.collection.deleteMany({
  422. testData: true
  423. });
  424. });
  425. after(async function () {
  426. await dataModule.shutdown();
  427. });
  428. });