DataModule.spec.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. // @ts-nocheck
  2. import async from "async";
  3. import chai, { expect } from "chai";
  4. import sinon from "sinon";
  5. import sinonChai from "sinon-chai";
  6. import chaiAsPromised from "chai-as-promised";
  7. import { ObjectId } from "mongodb";
  8. import JobContext from "../JobContext";
  9. import JobQueue from "../JobQueue";
  10. import LogBook from "../LogBook";
  11. import ModuleManager from "../ModuleManager";
  12. import DataModule from "./DataModule";
  13. chai.should();
  14. chai.use(sinonChai);
  15. chai.use(chaiAsPromised);
  16. describe("Data Module", function () {
  17. const moduleManager = Object.getPrototypeOf(
  18. sinon.createStubInstance(ModuleManager)
  19. );
  20. moduleManager.logBook = sinon.createStubInstance(LogBook);
  21. moduleManager.jobQueue = sinon.createStubInstance(JobQueue);
  22. const dataModule = new DataModule(moduleManager);
  23. const jobContext = sinon.createStubInstance(JobContext);
  24. const testData = { abc: [] };
  25. before(async function () {
  26. await dataModule.startup();
  27. dataModule.redisClient = sinon.spy(dataModule.redisClient);
  28. });
  29. beforeEach(async function () {
  30. testData.abc = await async.map(Array(10), async () => {
  31. const result =
  32. await dataModule.collections?.abc.collection.insertOne({
  33. _id: new ObjectId(),
  34. name: `Test${Math.round(Math.random() * 1000)}`,
  35. autofill: {
  36. enabled: !!Math.floor(Math.random())
  37. },
  38. someNumbers: Array.from({
  39. length: Math.max(1, Math.round(Math.random() * 50))
  40. }).map(() => Math.round(Math.random() * 10000)),
  41. songs: Array.from({
  42. length: Math.max(1, Math.round(Math.random() * 10))
  43. }).map(() => ({
  44. _id: new ObjectId()
  45. })),
  46. createdAt: Date.now(),
  47. updatedAt: Date.now(),
  48. testData: true
  49. });
  50. return dataModule.collections?.abc.collection.findOne({
  51. _id: result?.insertedId
  52. });
  53. });
  54. });
  55. it("module loaded and started", function () {
  56. moduleManager.logBook.log.should.have.been.called;
  57. dataModule.getName().should.equal("data");
  58. dataModule.getStatus().should.equal("STARTED");
  59. });
  60. describe("find job", function () {
  61. // Run cache test twice to validate mongo and redis sourced data
  62. [false, true, true].forEach(useCache => {
  63. it(`filter by one _id string ${
  64. useCache ? "with" : "without"
  65. } cache`, async function () {
  66. const [document] = testData.abc;
  67. const find = await dataModule.find(jobContext, {
  68. collection: "abc",
  69. filter: { _id: document._id },
  70. limit: 1,
  71. useCache
  72. });
  73. find.should.be.an("object");
  74. find._id.should.deep.equal(document._id);
  75. find.createdAt.should.deep.equal(new Date(document.createdAt));
  76. if (useCache) {
  77. dataModule.redisClient?.GET.should.have.been.called;
  78. }
  79. });
  80. });
  81. it(`filter by name string without cache`, async function () {
  82. const [document] = testData.abc;
  83. const find = await dataModule.find(jobContext, {
  84. collection: "abc",
  85. filter: { name: document.name },
  86. limit: 1,
  87. useCache: false
  88. });
  89. find.should.be.an("object");
  90. find._id.should.deep.equal(document._id);
  91. find.should.have.keys([
  92. "_id",
  93. "createdAt",
  94. "updatedAt",
  95. // "name", - Name is restricted, so it won't be returned
  96. "autofill",
  97. "someNumbers",
  98. "songs"
  99. ]);
  100. });
  101. it(`filter by normal array item`, async function () {
  102. const [document] = testData.abc;
  103. const resultDocument = await dataModule.find(jobContext, {
  104. collection: "abc",
  105. filter: { someNumbers: document.someNumbers[0] },
  106. limit: 1,
  107. useCache: false
  108. });
  109. resultDocument.should.be.an("object");
  110. resultDocument._id.should.deep.equal(document._id);
  111. });
  112. it(`filter by normal array item that doesn't exist`, async function () {
  113. const resultDocument = await dataModule.find(jobContext, {
  114. collection: "abc",
  115. filter: { someNumbers: -1 },
  116. limit: 1,
  117. useCache: false
  118. });
  119. expect(resultDocument).to.be.null;
  120. });
  121. it(`filter by schema array item`, async function () {
  122. const [document] = testData.abc;
  123. const resultDocument = await dataModule.find(jobContext, {
  124. collection: "abc",
  125. filter: { songs: { _id: document.songs[0]._id } },
  126. limit: 1,
  127. useCache: false
  128. });
  129. resultDocument.should.be.an("object");
  130. resultDocument._id.should.deep.equal(document._id);
  131. });
  132. it(`filter by schema array item, invalid`, async function () {
  133. const jobPromise = dataModule.find(jobContext, {
  134. collection: "abc",
  135. filter: { songs: { randomProperty: "Value" } },
  136. limit: 1,
  137. useCache: false
  138. });
  139. await expect(jobPromise).to.be.rejectedWith(
  140. `Key "randomProperty" does not exist in the schema.`
  141. );
  142. });
  143. it(`filter by schema array item with dot notation`, async function () {
  144. const [document] = testData.abc;
  145. const resultDocument = await dataModule.find(jobContext, {
  146. collection: "abc",
  147. filter: { "songs._id": document.songs[0]._id },
  148. limit: 1,
  149. useCache: false
  150. });
  151. resultDocument.should.be.an("object");
  152. resultDocument._id.should.deep.equal(document._id);
  153. });
  154. it(`filter by schema array item with dot notation, invalid`, async function () {
  155. const jobPromise = dataModule.find(jobContext, {
  156. collection: "abc",
  157. filter: { "songs.randomProperty": "Value" },
  158. limit: 1,
  159. useCache: false
  160. });
  161. await expect(jobPromise).to.be.rejectedWith(
  162. `Key "randomProperty" does not exist in the schema.`
  163. );
  164. });
  165. });
  166. describe("normalize projection", function () {
  167. const dataModuleProjection = Object.getPrototypeOf(dataModule);
  168. it(`basics`, function () {
  169. dataModuleProjection.normalizeProjection.should.be.a("function");
  170. });
  171. it(`empty object/array projection`, function () {
  172. const expectedResult = { projection: [], mode: "includeAllBut" };
  173. const resultWithArray = dataModuleProjection.normalizeProjection(
  174. []
  175. );
  176. const resultWithObject = dataModuleProjection.normalizeProjection(
  177. {}
  178. );
  179. resultWithArray.should.deep.equal(expectedResult);
  180. resultWithObject.should.deep.equal(expectedResult);
  181. });
  182. it(`null/undefined projection`, function () {
  183. const expectedResult = { projection: [], mode: "includeAllBut" };
  184. const resultWithNull =
  185. dataModuleProjection.normalizeProjection(null);
  186. const resultWithUndefined =
  187. dataModuleProjection.normalizeProjection(undefined);
  188. const resultWithNothing =
  189. dataModuleProjection.normalizeProjection();
  190. resultWithNull.should.deep.equal(expectedResult);
  191. resultWithUndefined.should.deep.equal(expectedResult);
  192. resultWithNothing.should.deep.equal(expectedResult);
  193. });
  194. it(`simple exclude projection`, function () {
  195. const expectedResult = {
  196. projection: [["name", false]],
  197. mode: "includeAllBut"
  198. };
  199. const resultWithBoolean = dataModuleProjection.normalizeProjection({
  200. name: false
  201. });
  202. const resultWithNumber = dataModuleProjection.normalizeProjection({
  203. name: 0
  204. });
  205. resultWithBoolean.should.deep.equal(expectedResult);
  206. resultWithNumber.should.deep.equal(expectedResult);
  207. });
  208. it(`simple include projection`, function () {
  209. const expectedResult = {
  210. projection: [["name", true]],
  211. mode: "excludeAllBut"
  212. };
  213. const resultWithObject = dataModuleProjection.normalizeProjection({
  214. name: true
  215. });
  216. const resultWithArray = dataModuleProjection.normalizeProjection([
  217. "name"
  218. ]);
  219. resultWithObject.should.deep.equal(expectedResult);
  220. resultWithArray.should.deep.equal(expectedResult);
  221. });
  222. it(`simple include/exclude projection`, function () {
  223. const expectedResult = {
  224. projection: [
  225. ["color", false],
  226. ["name", true]
  227. ],
  228. mode: "excludeAllBut"
  229. };
  230. const result = dataModuleProjection.normalizeProjection({
  231. color: false,
  232. name: true
  233. });
  234. result.should.deep.equal(expectedResult);
  235. });
  236. it(`simple nested include projection`, function () {
  237. const expectedResult = {
  238. projection: [["location.city", true]],
  239. mode: "excludeAllBut"
  240. };
  241. const resultWithObject = dataModuleProjection.normalizeProjection({
  242. location: {
  243. city: true
  244. }
  245. });
  246. const resultWithArray = dataModuleProjection.normalizeProjection([
  247. "location.city"
  248. ]);
  249. resultWithObject.should.deep.equal(expectedResult);
  250. resultWithArray.should.deep.equal(expectedResult);
  251. });
  252. it(`simple nested exclude projection`, function () {
  253. const expectedResult = {
  254. projection: [["location.city", false]],
  255. mode: "includeAllBut"
  256. };
  257. const result = dataModuleProjection.normalizeProjection({
  258. location: {
  259. city: false
  260. }
  261. });
  262. result.should.deep.equal(expectedResult);
  263. });
  264. it(`path collision`, function () {
  265. expect(() => {
  266. dataModuleProjection.normalizeProjection({
  267. location: {
  268. city: false
  269. },
  270. "location.city": true
  271. });
  272. }).to.throw("Path collision, non-unique key");
  273. });
  274. it(`path collision 2`, function () {
  275. expect(() => {
  276. dataModuleProjection.normalizeProjection({
  277. location: {
  278. city: {
  279. extra: false
  280. }
  281. },
  282. "location.city": true
  283. });
  284. }).to.throw(
  285. "Path collision! location.city.extra collides with location.city"
  286. );
  287. });
  288. // TODO add more test cases
  289. });
  290. afterEach(async function () {
  291. sinon.reset();
  292. await dataModule.collections?.abc.collection.deleteMany({
  293. testData: true
  294. });
  295. });
  296. after(async function () {
  297. await dataModule.shutdown();
  298. });
  299. });