ProjectLocator.test.mjs 18 KB

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