ProjectLocator.test.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. import { vi, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  4. const modulePath = '../../../../app/src/Features/Project/ProjectLocator'
  5. vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  6. vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  7. )
  8. const project = { _id: '1234566', rootFolder: [] }
  9. const rootDoc = { name: 'rootDoc', _id: 'das239djd' }
  10. const doc1 = { name: 'otherDoc.txt', _id: 'dsad2ddd' }
  11. const doc2 = { name: 'docname.txt', _id: 'dsad2ddddd' }
  12. const file1 = { name: 'file1', _id: 'dsa9lkdsad' }
  13. const subSubFile = { name: 'subSubFile', _id: 'd1d2dk' }
  14. const subSubDoc = { name: 'subdoc.txt', _id: '321dmdwi' }
  15. const firstSubFolder = {
  16. name: 'firstSubFolder',
  17. _id: 'rweq43',
  18. docs: [],
  19. fileRefs: [],
  20. folders: [],
  21. }
  22. const secondSubFolder = {
  23. name: 'secondSubFolder',
  24. _id: 'dsa3e23',
  25. docs: [subSubDoc],
  26. fileRefs: [subSubFile],
  27. folders: [],
  28. }
  29. const subFolder = {
  30. name: 'subFolder',
  31. _id: 'dsadsa93',
  32. folders: [secondSubFolder, null],
  33. docs: [],
  34. fileRefs: [],
  35. }
  36. const subFolder1 = {
  37. name: 'subFolder1',
  38. _id: '123asdjoij',
  39. folders: [firstSubFolder],
  40. }
  41. const rootFolder = {
  42. _id: '123sdskd',
  43. docs: [doc1, doc2, null, rootDoc],
  44. fileRefs: [file1],
  45. folders: [subFolder1, subFolder],
  46. }
  47. project.rootFolder[0] = rootFolder
  48. project.rootDoc_id = rootDoc._id
  49. describe('ProjectLocator', function () {
  50. beforeEach(async function (ctx) {
  51. ctx.ProjectGetter = {
  52. promises: {
  53. getProject: sinon.stub().resolves(project),
  54. },
  55. }
  56. ctx.ProjectHelper = {
  57. isArchived: sinon.stub(),
  58. isTrashed: sinon.stub(),
  59. isArchivedOrTrashed: sinon.stub(),
  60. }
  61. vi.doMock('../../../../app/src/models/User', () => ({
  62. default: { User: ctx.User },
  63. }))
  64. vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
  65. default: ctx.ProjectGetter,
  66. }))
  67. vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
  68. default: ctx.ProjectHelper,
  69. }))
  70. ctx.locator = (await import(modulePath)).default
  71. })
  72. describe('finding a doc', function () {
  73. it('finds one at the root level', async function (ctx) {
  74. const { element, path, folder } = await ctx.locator.promises.findElement({
  75. project_id: project._id,
  76. element_id: doc2._id,
  77. type: 'docs',
  78. })
  79. element._id.should.equal(doc2._id)
  80. path.fileSystem.should.equal(`/${doc2.name}`)
  81. folder._id.should.equal(project.rootFolder[0]._id)
  82. path.mongo.should.equal('rootFolder.0.docs.1')
  83. })
  84. it('when it is nested', async function (ctx) {
  85. const { element, path, folder } = await ctx.locator.promises.findElement({
  86. project_id: project._id,
  87. element_id: subSubDoc._id,
  88. type: 'doc',
  89. })
  90. expect(element._id).to.equal(subSubDoc._id)
  91. path.fileSystem.should.equal(
  92. `/${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}`
  93. )
  94. folder._id.should.equal(secondSubFolder._id)
  95. path.mongo.should.equal('rootFolder.0.folders.1.folders.0.docs.0')
  96. })
  97. it('should give error if element could not be found', async function (ctx) {
  98. await expect(
  99. ctx.locator.promises.findElement({
  100. project_id: project._id,
  101. element_id: 'ddsd432nj42',
  102. type: 'docs',
  103. })
  104. )
  105. .to.eventually.be.rejectedWith(Errors.NotFoundError)
  106. .and.eventually.have.property('message', 'entity not found')
  107. })
  108. })
  109. describe('finding a folder', function () {
  110. it('should return root folder when looking for root folder', async function (ctx) {
  111. const { element: foundElement } = await ctx.locator.promises.findElement({
  112. project_id: project._id,
  113. element_id: rootFolder._id,
  114. type: 'folder',
  115. })
  116. foundElement._id.should.equal(rootFolder._id)
  117. })
  118. it('should not return root folder when searching for docs', async function (ctx) {
  119. await expect(
  120. ctx.locator.promises.findElement({
  121. project_id: project._id,
  122. element_id: rootFolder._id,
  123. type: 'docs',
  124. })
  125. )
  126. .to.eventually.be.rejectedWith(Errors.NotFoundError)
  127. .and.eventually.have.property('message', 'entity not found')
  128. })
  129. it('should not return root folder when searching for files', async function (ctx) {
  130. await expect(
  131. ctx.locator.promises.findElement({
  132. project_id: project._id,
  133. element_id: rootFolder._id,
  134. type: 'files',
  135. })
  136. )
  137. .to.eventually.be.rejectedWith(Errors.NotFoundError)
  138. .and.eventually.have.property('message', 'entity not found')
  139. })
  140. it('when at root', async function (ctx) {
  141. const {
  142. element: foundElement,
  143. path,
  144. folder: parentFolder,
  145. } = await ctx.locator.promises.findElement({
  146. project_id: project._id,
  147. element_id: subFolder._id,
  148. type: 'folder',
  149. })
  150. foundElement._id.should.equal(subFolder._id)
  151. path.fileSystem.should.equal(`/${subFolder.name}`)
  152. parentFolder._id.should.equal(rootFolder._id)
  153. path.mongo.should.equal('rootFolder.0.folders.1')
  154. })
  155. it('when deeply nested', async function (ctx) {
  156. const {
  157. element: foundElement,
  158. path,
  159. folder: parentFolder,
  160. } = await ctx.locator.promises.findElement({
  161. project_id: project._id,
  162. element_id: secondSubFolder._id,
  163. type: 'folder',
  164. })
  165. foundElement._id.should.equal(secondSubFolder._id)
  166. path.fileSystem.should.equal(`/${subFolder.name}/${secondSubFolder.name}`)
  167. parentFolder._id.should.equal(subFolder._id)
  168. path.mongo.should.equal('rootFolder.0.folders.1.folders.0')
  169. })
  170. })
  171. describe('finding a file', function () {
  172. it('when at root', async function (ctx) {
  173. const {
  174. element: foundElement,
  175. path,
  176. folder: parentFolder,
  177. } = await ctx.locator.promises.findElement({
  178. project_id: project._id,
  179. element_id: file1._id,
  180. type: 'fileRefs',
  181. })
  182. foundElement._id.should.equal(file1._id)
  183. path.fileSystem.should.equal(`/${file1.name}`)
  184. parentFolder._id.should.equal(rootFolder._id)
  185. path.mongo.should.equal('rootFolder.0.fileRefs.0')
  186. })
  187. it('when deeply nested', async function (ctx) {
  188. const {
  189. element: foundElement,
  190. path,
  191. folder: parentFolder,
  192. } = await ctx.locator.promises.findElement({
  193. project_id: project._id,
  194. element_id: subSubFile._id,
  195. type: 'fileRefs',
  196. })
  197. foundElement._id.should.equal(subSubFile._id)
  198. path.fileSystem.should.equal(
  199. `/${subFolder.name}/${secondSubFolder.name}/${subSubFile.name}`
  200. )
  201. parentFolder._id.should.equal(secondSubFolder._id)
  202. path.mongo.should.equal('rootFolder.0.folders.1.folders.0.fileRefs.0')
  203. })
  204. })
  205. describe('finding an element with wrong element type', function () {
  206. it('should add an s onto the element type', async function (ctx) {
  207. const { element: foundElement } = await ctx.locator.promises.findElement({
  208. project_id: project._id,
  209. element_id: subSubDoc._id,
  210. type: 'doc',
  211. })
  212. foundElement._id.should.equal(subSubDoc._id)
  213. })
  214. it('should convert file to fileRefs', async function (ctx) {
  215. const { element: foundElement } = await ctx.locator.promises.findElement({
  216. project_id: project._id,
  217. element_id: file1._id,
  218. type: 'fileRefs',
  219. })
  220. foundElement._id.should.equal(file1._id)
  221. })
  222. })
  223. describe('should be able to take actual project as well as id', function () {
  224. const doc3 = {
  225. _id: '123dsdj3',
  226. name: 'doc3',
  227. }
  228. const rootFolder2 = {
  229. _id: '123sddedskd',
  230. docs: [doc3],
  231. }
  232. const project2 = {
  233. _id: '1234566',
  234. rootFolder: [rootFolder2],
  235. }
  236. it('should find doc in project', async function (ctx) {
  237. const {
  238. element: foundElement,
  239. path,
  240. folder: parentFolder,
  241. } = await ctx.locator.promises.findElement({
  242. project: project2,
  243. element_id: doc3._id,
  244. type: 'docs',
  245. })
  246. foundElement._id.should.equal(doc3._id)
  247. path.fileSystem.should.equal(`/${doc3.name}`)
  248. parentFolder._id.should.equal(project2.rootFolder[0]._id)
  249. path.mongo.should.equal('rootFolder.0.docs.0')
  250. })
  251. })
  252. describe('finding root doc', function () {
  253. it('should return root doc when passed project', async function (ctx) {
  254. const { element: doc } = await ctx.locator.promises.findRootDoc(project)
  255. doc._id.should.equal(rootDoc._id)
  256. })
  257. it('should return root doc when passed project_id', async function (ctx) {
  258. const { element: doc } = await ctx.locator.promises.findRootDoc(
  259. project._id
  260. )
  261. doc._id.should.equal(rootDoc._id)
  262. })
  263. it('should return null when the project has no rootDoc', async function (ctx) {
  264. project.rootDoc_id = null
  265. const { element: rootDoc } =
  266. await ctx.locator.promises.findRootDoc(project)
  267. expect(rootDoc).to.equal(null)
  268. })
  269. it('should return null when the rootDoc_id no longer exists', async function (ctx) {
  270. project.rootDoc_id = 'doesntexist'
  271. const { element: rootDoc } =
  272. await ctx.locator.promises.findRootDoc(project)
  273. expect(rootDoc).to.equal(null)
  274. })
  275. })
  276. describe('findElementByPath', function () {
  277. it('should take a doc path and return the element for a root level document', async function (ctx) {
  278. const path = `${doc1.name}`
  279. const { element, type, folder } =
  280. await ctx.locator.promises.findElementByPath({
  281. project,
  282. path,
  283. })
  284. element.should.deep.equal(doc1)
  285. expect(type).to.equal('doc')
  286. expect(folder).to.equal(rootFolder)
  287. })
  288. it('should take a doc path and return the element for a root level document with a starting slash', async function (ctx) {
  289. const path = `/${doc1.name}`
  290. const { element, type, folder } =
  291. await ctx.locator.promises.findElementByPath({
  292. project,
  293. path,
  294. })
  295. element.should.deep.equal(doc1)
  296. expect(type).to.equal('doc')
  297. expect(folder).to.equal(rootFolder)
  298. })
  299. it('should take a doc path and return the element for a nested document', async function (ctx) {
  300. const path = `${subFolder.name}/${secondSubFolder.name}/${subSubDoc.name}`
  301. const { element, type, folder } =
  302. await ctx.locator.promises.findElementByPath({
  303. project,
  304. path,
  305. })
  306. element.should.deep.equal(subSubDoc)
  307. expect(type).to.equal('doc')
  308. expect(folder).to.equal(secondSubFolder)
  309. })
  310. it('should take a file path and return the element for a root level document', async function (ctx) {
  311. const path = `${file1.name}`
  312. const { element, type, folder } =
  313. await ctx.locator.promises.findElementByPath({
  314. project,
  315. path,
  316. })
  317. element.should.deep.equal(file1)
  318. expect(type).to.equal('file')
  319. expect(folder).to.equal(rootFolder)
  320. })
  321. it('should take a file path and return the element for a nested document', async function (ctx) {
  322. const path = `${subFolder.name}/${secondSubFolder.name}/${subSubFile.name}`
  323. const { element, type, folder } =
  324. await ctx.locator.promises.findElementByPath({
  325. project,
  326. path,
  327. })
  328. element.should.deep.equal(subSubFile)
  329. expect(type).to.equal('file')
  330. expect(folder).to.equal(secondSubFolder)
  331. })
  332. it('should take a file path and return the element for a nested document case insenstive', async function (ctx) {
  333. const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}`
  334. const { element, type, folder } =
  335. await ctx.locator.promises.findElementByPath({
  336. project,
  337. path,
  338. })
  339. element.should.deep.equal(subSubFile)
  340. expect(type).to.equal('file')
  341. expect(folder).to.equal(secondSubFolder)
  342. })
  343. it('should not return elements with a case-insensitive match when exactCaseMatch is true', async function (ctx) {
  344. const path = `${subFolder.name.toUpperCase()}/${secondSubFolder.name.toUpperCase()}/${subSubFile.name.toUpperCase()}`
  345. await expect(
  346. ctx.locator.promises.findElementByPath({
  347. project,
  348. path,
  349. exactCaseMatch: true,
  350. })
  351. ).to.eventually.be.rejected
  352. })
  353. it('should take a file path and return the element for a nested folder', async function (ctx) {
  354. const path = `${subFolder.name}/${secondSubFolder.name}`
  355. const { element, type, folder } =
  356. await ctx.locator.promises.findElementByPath({
  357. project,
  358. path,
  359. })
  360. element.should.deep.equal(secondSubFolder)
  361. expect(type).to.equal('folder')
  362. expect(folder).to.equal(subFolder)
  363. })
  364. it('should take a file path and return the root folder', async function (ctx) {
  365. const path = '/'
  366. const { element, type, folder } =
  367. await ctx.locator.promises.findElementByPath({
  368. project,
  369. path,
  370. })
  371. element.should.deep.equal(rootFolder)
  372. expect(type).to.equal('folder')
  373. expect(folder).to.equal(null)
  374. })
  375. it('should return an error if the file can not be found inside know folder', async function (ctx) {
  376. const path = `${subFolder.name}/${secondSubFolder.name}/exist.txt`
  377. await expect(ctx.locator.promises.findElementByPath({ project, path })).to
  378. .eventually.be.rejected
  379. })
  380. it('should return an error if the file can not be found inside unknown folder', async function (ctx) {
  381. const path = 'this/does/not/exist.txt'
  382. await expect(
  383. ctx.locator.promises.findElementByPath({
  384. project,
  385. path,
  386. })
  387. ).to.eventually.be.rejected
  388. })
  389. describe('where duplicate folder exists', function () {
  390. beforeEach(function (ctx) {
  391. ctx.duplicateFolder = {
  392. name: 'duplicate1',
  393. _id: '1234',
  394. folders: [
  395. {
  396. name: '1',
  397. docs: [{ name: 'main.tex', _id: '456' }],
  398. folders: [],
  399. fileRefs: [],
  400. },
  401. ],
  402. docs: [(ctx.doc = { name: 'main.tex', _id: '456' })],
  403. fileRefs: [],
  404. }
  405. ctx.project = {
  406. rootFolder: [
  407. {
  408. folders: [ctx.duplicateFolder, ctx.duplicateFolder],
  409. fileRefs: [],
  410. docs: [],
  411. },
  412. ],
  413. }
  414. })
  415. it('should not call the callback more than once', async function (ctx) {
  416. const path = `${ctx.duplicateFolder.name}/${ctx.doc.name}`
  417. await ctx.locator.promises.findElementByPath({
  418. project: ctx.project,
  419. path,
  420. })
  421. }) // mocha will throw exception if done called multiple times
  422. it('should not call the callback more than once when the path is longer than 1 level below the duplicate level', async function (ctx) {
  423. const path = `${ctx.duplicateFolder.name}/1/main.tex`
  424. await ctx.locator.promises.findElementByPath({
  425. project: ctx.project,
  426. path,
  427. })
  428. })
  429. }) // mocha will throw exception if done called multiple times
  430. describe('with a null doc', function () {
  431. beforeEach(function (ctx) {
  432. ctx.project = {
  433. rootFolder: [
  434. {
  435. folders: [],
  436. fileRefs: [],
  437. docs: [{ name: 'main.tex' }, null, { name: 'other.tex' }],
  438. },
  439. ],
  440. }
  441. })
  442. it('should not crash with a null', async function (ctx) {
  443. const path = '/other.tex'
  444. const { element } = await ctx.locator.promises.findElementByPath({
  445. project: ctx.project,
  446. path,
  447. })
  448. element.name.should.equal('other.tex')
  449. })
  450. })
  451. describe('with a null project', function () {
  452. beforeEach(function (ctx) {
  453. ctx.ProjectGetter = {
  454. promises: {
  455. getProject: sinon.stub().resolves(null),
  456. },
  457. }
  458. })
  459. it('should not crash with a null', async function (ctx) {
  460. const path = '/other.tex'
  461. await expect(
  462. ctx.locator.promises.findElementByPath({
  463. project_id: project._id,
  464. path,
  465. })
  466. ).to.be.rejected
  467. })
  468. })
  469. describe('with a project_id', function () {
  470. it('should take a doc path and return the element for a root level document', async function (ctx) {
  471. const path = `${doc1.name}`
  472. const { element, type } = await ctx.locator.promises.findElementByPath({
  473. project_id: project._id,
  474. path,
  475. })
  476. ctx.ProjectGetter.promises.getProject
  477. .calledWith(project._id, { rootFolder: true, rootDoc_id: true })
  478. .should.equal(true)
  479. element.should.deep.equal(doc1)
  480. expect(type).to.equal('doc')
  481. })
  482. })
  483. })
  484. describe('findElementByMongoPath', function () {
  485. it('traverses the file tree like Mongo would do', function (ctx) {
  486. const element = ctx.locator.findElementByMongoPath(
  487. project,
  488. 'rootFolder.0.folders.1.folders.0.fileRefs.0'
  489. )
  490. expect(element).to.equal(subSubFile)
  491. })
  492. it('throws an error if no element is found', function (ctx) {
  493. expect(() =>
  494. ctx.locator.findElementByMongoPath(
  495. project,
  496. 'rootolder.0.folders.0.folders.0.fileRefs.0'
  497. )
  498. ).to.throw
  499. })
  500. })
  501. })