DataModule.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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. const should = 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. describe("filter by date types", function () {
  297. it("Date", async function () {
  298. const [document] = testData.abc;
  299. const { createdAt } = document;
  300. const resultDocument = await dataModule.find(jobContext, {
  301. collection: "abc",
  302. filter: { createdAt },
  303. limit: 1,
  304. useCache: false
  305. });
  306. should.exist(resultDocument);
  307. resultDocument.createdAt.should.deep.equal(document.createdAt);
  308. });
  309. it("String", async function () {
  310. const [document] = testData.abc;
  311. const { createdAt } = document;
  312. const resultDocument = await dataModule.find(jobContext, {
  313. collection: "abc",
  314. filter: { createdAt: createdAt.toString() },
  315. limit: 1,
  316. useCache: false
  317. });
  318. should.exist(resultDocument);
  319. resultDocument.createdAt.should.deep.equal(document.createdAt);
  320. });
  321. it("Number", async function () {
  322. const [document] = testData.abc;
  323. const { createdAt } = document;
  324. const resultDocument = await dataModule.find(jobContext, {
  325. collection: "abc",
  326. filter: { createdAt: createdAt.getTime() },
  327. limit: 1,
  328. useCache: false
  329. });
  330. should.exist(resultDocument);
  331. resultDocument.createdAt.should.deep.equal(document.createdAt);
  332. });
  333. });
  334. });
  335. describe("normalize projection", function () {
  336. const dataModuleProjection = Object.getPrototypeOf(dataModule);
  337. it(`basics`, function () {
  338. dataModuleProjection.normalizeProjection.should.be.a("function");
  339. });
  340. it(`empty object/array projection`, function () {
  341. const expectedResult = { projection: [], mode: "includeAllBut" };
  342. const resultWithArray = dataModuleProjection.normalizeProjection(
  343. []
  344. );
  345. const resultWithObject = dataModuleProjection.normalizeProjection(
  346. {}
  347. );
  348. resultWithArray.should.deep.equal(expectedResult);
  349. resultWithObject.should.deep.equal(expectedResult);
  350. });
  351. it(`null/undefined projection`, function () {
  352. const expectedResult = { projection: [], mode: "includeAllBut" };
  353. const resultWithNull =
  354. dataModuleProjection.normalizeProjection(null);
  355. const resultWithUndefined =
  356. dataModuleProjection.normalizeProjection(undefined);
  357. const resultWithNothing =
  358. dataModuleProjection.normalizeProjection();
  359. resultWithNull.should.deep.equal(expectedResult);
  360. resultWithUndefined.should.deep.equal(expectedResult);
  361. resultWithNothing.should.deep.equal(expectedResult);
  362. });
  363. it(`simple exclude projection`, function () {
  364. const expectedResult = {
  365. projection: [["name", false]],
  366. mode: "includeAllBut"
  367. };
  368. const resultWithBoolean = dataModuleProjection.normalizeProjection({
  369. name: false
  370. });
  371. const resultWithNumber = dataModuleProjection.normalizeProjection({
  372. name: 0
  373. });
  374. resultWithBoolean.should.deep.equal(expectedResult);
  375. resultWithNumber.should.deep.equal(expectedResult);
  376. });
  377. it(`simple include projection`, function () {
  378. const expectedResult = {
  379. projection: [["name", true]],
  380. mode: "excludeAllBut"
  381. };
  382. const resultWithObject = dataModuleProjection.normalizeProjection({
  383. name: true
  384. });
  385. const resultWithArray = dataModuleProjection.normalizeProjection([
  386. "name"
  387. ]);
  388. resultWithObject.should.deep.equal(expectedResult);
  389. resultWithArray.should.deep.equal(expectedResult);
  390. });
  391. it(`simple include/exclude projection`, function () {
  392. const expectedResult = {
  393. projection: [
  394. ["color", false],
  395. ["name", true]
  396. ],
  397. mode: "excludeAllBut"
  398. };
  399. const result = dataModuleProjection.normalizeProjection({
  400. color: false,
  401. name: true
  402. });
  403. result.should.deep.equal(expectedResult);
  404. });
  405. it(`simple nested include projection`, function () {
  406. const expectedResult = {
  407. projection: [["location.city", true]],
  408. mode: "excludeAllBut"
  409. };
  410. const resultWithObject = dataModuleProjection.normalizeProjection({
  411. location: {
  412. city: true
  413. }
  414. });
  415. const resultWithArray = dataModuleProjection.normalizeProjection([
  416. "location.city"
  417. ]);
  418. resultWithObject.should.deep.equal(expectedResult);
  419. resultWithArray.should.deep.equal(expectedResult);
  420. });
  421. it(`simple nested exclude projection`, function () {
  422. const expectedResult = {
  423. projection: [["location.city", false]],
  424. mode: "includeAllBut"
  425. };
  426. const result = dataModuleProjection.normalizeProjection({
  427. location: {
  428. city: false
  429. }
  430. });
  431. result.should.deep.equal(expectedResult);
  432. });
  433. it(`path collision`, function () {
  434. (() =>
  435. dataModuleProjection.normalizeProjection({
  436. location: {
  437. city: false
  438. },
  439. "location.city": true
  440. })).should.throw("Path collision, non-unique key");
  441. });
  442. it(`path collision 2`, function () {
  443. (() =>
  444. dataModuleProjection.normalizeProjection({
  445. location: {
  446. city: {
  447. extra: false
  448. }
  449. },
  450. "location.city": true
  451. })).should.throw(
  452. "Path collision! location.city.extra collides with location.city"
  453. );
  454. });
  455. // TODO add more test cases
  456. });
  457. afterEach(async function () {
  458. sinon.reset();
  459. await dataModule.collections?.abc.collection.deleteMany({
  460. testData: true
  461. });
  462. });
  463. after(async function () {
  464. await dataModule.shutdown();
  465. });
  466. });