ProjectEntityMongoUpdateHandlerTests.js 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  1. const { expect } = require('chai')
  2. const sinon = require('sinon')
  3. const tk = require('timekeeper')
  4. const Errors = require('../../../../app/src/Features/Errors/Errors')
  5. const { ObjectId } = require('mongodb')
  6. const SandboxedModule = require('sandboxed-module')
  7. const { DeletedFile } = require('../helpers/models/DeletedFile')
  8. const { Project } = require('../helpers/models/Project')
  9. const MODULE_PATH =
  10. '../../../../app/src/Features/Project/ProjectEntityMongoUpdateHandler'
  11. describe('ProjectEntityMongoUpdateHandler', function () {
  12. beforeEach(function () {
  13. this.doc = {
  14. _id: ObjectId(),
  15. name: 'test-doc.txt',
  16. lines: ['hello', 'world'],
  17. rev: 1234,
  18. }
  19. this.docPath = {
  20. mongo: 'rootFolder.0.docs.0',
  21. fileSystem: '/test-doc.txt',
  22. }
  23. this.file = {
  24. _id: ObjectId(),
  25. name: 'something.jpg',
  26. linkedFileData: { provider: 'url' },
  27. hash: 'some-hash',
  28. }
  29. this.filePath = {
  30. fileSystem: '/something.png',
  31. mongo: 'rootFolder.0.fileRefs.0',
  32. }
  33. this.subfolder = { _id: ObjectId(), name: 'test-subfolder' }
  34. this.subfolderPath = {
  35. fileSystem: '/test-folder/test-subfolder',
  36. mongo: 'rootFolder.0.folders.0.folders.0',
  37. }
  38. this.folder = {
  39. _id: ObjectId(),
  40. name: 'test-folder',
  41. folders: [this.subfolder],
  42. }
  43. this.folderPath = {
  44. fileSystem: '/test-folder',
  45. mongo: 'rootFolder.0.folders.0',
  46. }
  47. this.rootFolder = {
  48. _id: ObjectId(),
  49. folders: [this.folder],
  50. docs: [this.doc],
  51. fileRefs: [this.file],
  52. }
  53. this.rootFolderPath = {
  54. fileSystem: '/',
  55. mongo: 'rootFolder.0',
  56. }
  57. this.project = {
  58. _id: ObjectId(),
  59. name: 'project name',
  60. rootFolder: [this.rootFolder],
  61. }
  62. this.Settings = { maxEntitiesPerProject: 100 }
  63. this.CooldownManager = {}
  64. this.LockManager = {
  65. promises: {
  66. runWithLock: sinon.spy((namespace, id, runner) => runner()),
  67. },
  68. }
  69. this.FolderModel = sinon.stub()
  70. this.DeletedFileMock = sinon.mock(DeletedFile)
  71. this.ProjectMock = sinon.mock(Project)
  72. this.ProjectEntityHandler = {
  73. getAllEntitiesFromProject: sinon.stub(),
  74. }
  75. this.ProjectLocator = {
  76. findElementByMongoPath: sinon.stub().throws(new Error('not found')),
  77. promises: {
  78. findElement: sinon.stub().rejects(new Error('not found')),
  79. findElementByPath: sinon.stub().rejects(new Error('not found')),
  80. },
  81. }
  82. this.ProjectLocator.promises.findElement
  83. .withArgs({
  84. project: this.project,
  85. element_id: this.rootFolder._id,
  86. type: 'folder',
  87. })
  88. .resolves({
  89. element: this.rootFolder,
  90. path: this.rootFolderPath,
  91. })
  92. this.ProjectLocator.promises.findElement
  93. .withArgs({
  94. project: this.project,
  95. element_id: this.folder._id,
  96. type: 'folder',
  97. })
  98. .resolves({
  99. element: this.folder,
  100. path: this.folderPath,
  101. folder: this.rootFolder,
  102. })
  103. this.ProjectLocator.promises.findElement
  104. .withArgs({
  105. project: this.project,
  106. element_id: this.subfolder._id,
  107. type: 'folder',
  108. })
  109. .resolves({
  110. element: this.subfolder,
  111. path: this.subfolderPath,
  112. folder: this.folder,
  113. })
  114. this.ProjectLocator.promises.findElement
  115. .withArgs({
  116. project: this.project,
  117. element_id: this.file._id,
  118. type: 'file',
  119. })
  120. .resolves({
  121. element: this.file,
  122. path: this.filePath,
  123. folder: this.rootFolder,
  124. })
  125. this.ProjectLocator.promises.findElement
  126. .withArgs({
  127. project: this.project,
  128. element_id: this.doc._id,
  129. type: 'doc',
  130. })
  131. .resolves({
  132. element: this.doc,
  133. path: this.docPath,
  134. folder: this.rootFolder,
  135. })
  136. this.ProjectLocator.promises.findElementByPath
  137. .withArgs(
  138. sinon.match({
  139. project: this.project,
  140. path: '/',
  141. })
  142. )
  143. .resolves({ element: this.rootFolder, type: 'folder', folder: null })
  144. this.ProjectLocator.promises.findElementByPath
  145. .withArgs(
  146. sinon.match({
  147. project: this.project,
  148. path: '/test-folder',
  149. })
  150. )
  151. .resolves({
  152. element: this.folder,
  153. type: 'folder',
  154. folder: this.rootFolder,
  155. })
  156. this.ProjectLocator.promises.findElementByPath
  157. .withArgs(
  158. sinon.match({
  159. project: this.project,
  160. path: '/test-folder/test-subfolder',
  161. })
  162. )
  163. .resolves({
  164. element: this.subfolder,
  165. type: 'folder',
  166. folder: this.folder,
  167. })
  168. this.ProjectGetter = {
  169. promises: {
  170. getProjectWithoutLock: sinon
  171. .stub()
  172. .withArgs(this.project._id)
  173. .resolves(this.project),
  174. getProjectWithOnlyFolders: sinon.stub().resolves(this.project),
  175. },
  176. }
  177. this.FolderStructureBuilder = {
  178. buildFolderStructure: sinon.stub(),
  179. }
  180. this.subject = SandboxedModule.require(MODULE_PATH, {
  181. requires: {
  182. mongodb: { ObjectId },
  183. '@overleaf/settings': this.Settings,
  184. '../Cooldown/CooldownManager': this.CooldownManager,
  185. '../../models/Folder': { Folder: this.FolderModel },
  186. '../../infrastructure/LockManager': this.LockManager,
  187. '../../models/DeletedFile': { DeletedFile },
  188. '../../models/Project': { Project },
  189. './ProjectEntityHandler': this.ProjectEntityHandler,
  190. './ProjectLocator': this.ProjectLocator,
  191. './ProjectGetter': this.ProjectGetter,
  192. './FolderStructureBuilder': this.FolderStructureBuilder,
  193. },
  194. })
  195. })
  196. afterEach(function () {
  197. this.DeletedFileMock.restore()
  198. this.ProjectMock.restore()
  199. })
  200. beforeEach(function () {
  201. tk.freeze(Date.now())
  202. })
  203. afterEach(function () {
  204. tk.reset()
  205. })
  206. describe('addDoc', function () {
  207. beforeEach(async function () {
  208. const doc = { _id: ObjectId(), name: 'other.txt' }
  209. this.ProjectMock.expects('findOneAndUpdate')
  210. .withArgs(
  211. {
  212. _id: this.project._id,
  213. 'rootFolder.0.folders.0': { $exists: true },
  214. },
  215. {
  216. $push: { 'rootFolder.0.folders.0.docs': doc },
  217. $inc: { version: 1 },
  218. }
  219. )
  220. .chain('exec')
  221. .resolves(this.project)
  222. this.result = await this.subject.promises.addDoc(
  223. this.project._id,
  224. this.folder._id,
  225. doc
  226. )
  227. })
  228. it('adds the document in Mongo', function () {
  229. this.ProjectMock.verify()
  230. })
  231. it('returns path info and the project', function () {
  232. expect(this.result).to.deep.equal({
  233. result: {
  234. path: {
  235. mongo: 'rootFolder.0.folders.0',
  236. fileSystem: '/test-folder/other.txt',
  237. },
  238. },
  239. project: this.project,
  240. })
  241. })
  242. })
  243. describe('addFile', function () {
  244. beforeEach(function () {
  245. this.newFile = { _id: ObjectId(), name: 'picture.jpg' }
  246. this.ProjectMock.expects('findOneAndUpdate')
  247. .withArgs(
  248. {
  249. _id: this.project._id,
  250. 'rootFolder.0.folders.0': { $exists: true },
  251. },
  252. {
  253. $push: { 'rootFolder.0.folders.0.fileRefs': this.newFile },
  254. $inc: { version: 1 },
  255. }
  256. )
  257. .chain('exec')
  258. .resolves(this.project)
  259. })
  260. describe('happy path', function () {
  261. beforeEach(async function () {
  262. this.result = await this.subject.promises.addFile(
  263. this.project._id,
  264. this.folder._id,
  265. this.newFile
  266. )
  267. })
  268. it('adds the file in Mongo', function () {
  269. this.ProjectMock.verify()
  270. })
  271. it('returns path info and the project', function () {
  272. expect(this.result).to.deep.equal({
  273. result: {
  274. path: {
  275. mongo: 'rootFolder.0.folders.0',
  276. fileSystem: '/test-folder/picture.jpg',
  277. },
  278. },
  279. project: this.project,
  280. })
  281. })
  282. })
  283. describe('when entity limit is reached', function () {
  284. beforeEach(function () {
  285. this.savedMaxEntities = this.Settings.maxEntitiesPerProject
  286. this.Settings.maxEntitiesPerProject = 3
  287. })
  288. afterEach(function () {
  289. this.Settings.maxEntitiesPerProject = this.savedMaxEntities
  290. })
  291. it('should throw an error', async function () {
  292. await expect(
  293. this.subject.promises.addFile(
  294. this.project._id,
  295. this.folder._id,
  296. this.newFile
  297. )
  298. ).to.be.rejected
  299. })
  300. })
  301. })
  302. describe('addFolder', function () {
  303. beforeEach(async function () {
  304. const folderName = 'New folder'
  305. this.FolderModel.withArgs({ name: folderName }).returns({
  306. _id: ObjectId(),
  307. name: folderName,
  308. })
  309. this.ProjectMock.expects('findOneAndUpdate')
  310. .withArgs(
  311. {
  312. _id: this.project._id,
  313. 'rootFolder.0.folders.0': { $exists: true },
  314. },
  315. {
  316. $push: {
  317. 'rootFolder.0.folders.0.folders': sinon.match({
  318. name: folderName,
  319. }),
  320. },
  321. $inc: { version: 1 },
  322. }
  323. )
  324. .chain('exec')
  325. .resolves(this.project)
  326. await this.subject.promises.addFolder(
  327. this.project._id,
  328. this.folder._id,
  329. folderName
  330. )
  331. })
  332. it('adds the folder in Mongo', function () {
  333. this.ProjectMock.verify()
  334. })
  335. })
  336. describe('replaceFileWithNew', function () {
  337. beforeEach(async function () {
  338. const newFile = {
  339. _id: ObjectId(),
  340. name: 'some-other-file.png',
  341. linkedFileData: { some: 'data' },
  342. hash: 'some-hash',
  343. }
  344. // Add a deleted file record
  345. this.DeletedFileMock.expects('create')
  346. .withArgs({
  347. projectId: this.project._id,
  348. _id: this.file._id,
  349. name: this.file.name,
  350. linkedFileData: this.file.linkedFileData,
  351. hash: this.file.hash,
  352. deletedAt: sinon.match.date,
  353. })
  354. .resolves()
  355. // Update the file in place
  356. this.ProjectMock.expects('findOneAndUpdate')
  357. .withArgs(
  358. {
  359. _id: this.project._id,
  360. 'rootFolder.0.fileRefs.0': { $exists: true },
  361. },
  362. {
  363. $set: {
  364. 'rootFolder.0.fileRefs.0._id': newFile._id,
  365. 'rootFolder.0.fileRefs.0.created': sinon.match.date,
  366. 'rootFolder.0.fileRefs.0.linkedFileData': newFile.linkedFileData,
  367. 'rootFolder.0.fileRefs.0.hash': newFile.hash,
  368. },
  369. $inc: {
  370. version: 1,
  371. 'rootFolder.0.fileRefs.0.rev': 1,
  372. },
  373. }
  374. )
  375. .chain('exec')
  376. .resolves(this.project)
  377. this.ProjectLocator.findElementByMongoPath
  378. .withArgs(this.project, 'rootFolder.0.fileRefs.0')
  379. .returns(newFile)
  380. await this.subject.promises.replaceFileWithNew(
  381. this.project._id,
  382. this.file._id,
  383. newFile
  384. )
  385. })
  386. it('updates the database', function () {
  387. this.DeletedFileMock.verify()
  388. this.ProjectMock.verify()
  389. })
  390. })
  391. describe('mkdirp', function () {
  392. describe('when the path is just a slash', function () {
  393. beforeEach(async function () {
  394. this.result = await this.subject.promises.mkdirp(this.project._id, '/')
  395. })
  396. it('should return the root folder', function () {
  397. expect(this.result.folder).to.deep.equal(this.rootFolder)
  398. })
  399. it('should not report a parent folder', function () {
  400. expect(this.result.folder.parentFolder_id).not.to.exist
  401. })
  402. it('should not return new folders', function () {
  403. expect(this.result.newFolders).to.have.length(0)
  404. })
  405. })
  406. describe('when the folder already exists', function () {
  407. beforeEach(async function () {
  408. this.result = await this.subject.promises.mkdirp(
  409. this.project._id,
  410. '/test-folder'
  411. )
  412. })
  413. it('should return the existing folder', function () {
  414. expect(this.result.folder).to.deep.equal(this.folder)
  415. })
  416. it('should report the parent folder', function () {
  417. expect(this.result.folder.parentFolder_id).to.equal(this.rootFolder._id)
  418. })
  419. it('should not return new folders', function () {
  420. expect(this.result.newFolders).to.have.length(0)
  421. })
  422. })
  423. describe('when the path is a new folder at the top level', function () {
  424. beforeEach(async function () {
  425. this.newFolder = { _id: ObjectId(), name: 'new-folder' }
  426. this.FolderModel.returns(this.newFolder)
  427. this.exactCaseMatch = false
  428. this.ProjectMock.expects('findOneAndUpdate')
  429. .withArgs(
  430. { _id: this.project._id, 'rootFolder.0': { $exists: true } },
  431. {
  432. $push: { 'rootFolder.0.folders': this.newFolder },
  433. $inc: { version: 1 },
  434. }
  435. )
  436. .chain('exec')
  437. .resolves(this.project)
  438. this.result = await this.subject.promises.mkdirp(
  439. this.project._id,
  440. '/new-folder/',
  441. { exactCaseMatch: this.exactCaseMatch }
  442. )
  443. })
  444. it('should update the database', function () {
  445. this.ProjectMock.verify()
  446. })
  447. it('should make just one folder', function () {
  448. expect(this.result.newFolders).to.have.length(1)
  449. })
  450. it('should return the new folder', function () {
  451. expect(this.result.folder.name).to.equal('new-folder')
  452. })
  453. it('should return the parent folder', function () {
  454. expect(this.result.folder.parentFolder_id).to.equal(this.rootFolder._id)
  455. })
  456. it('should pass the exactCaseMatch option to ProjectLocator', function () {
  457. expect(
  458. this.ProjectLocator.promises.findElementByPath
  459. ).to.have.been.calledWithMatch({ exactCaseMatch: this.exactCaseMatch })
  460. })
  461. })
  462. describe('adding a subfolder', function () {
  463. beforeEach(async function () {
  464. this.newFolder = { _id: ObjectId(), name: 'new-folder' }
  465. this.FolderModel.returns(this.newFolder)
  466. this.ProjectMock.expects('findOneAndUpdate')
  467. .withArgs(
  468. {
  469. _id: this.project._id,
  470. 'rootFolder.0.folders.0': { $exists: true },
  471. },
  472. {
  473. $push: {
  474. 'rootFolder.0.folders.0.folders': sinon.match({
  475. name: 'new-folder',
  476. }),
  477. },
  478. $inc: { version: 1 },
  479. }
  480. )
  481. .chain('exec')
  482. .resolves(this.project)
  483. this.result = await this.subject.promises.mkdirp(
  484. this.project._id,
  485. '/test-folder/new-folder'
  486. )
  487. })
  488. it('should update the database', function () {
  489. this.ProjectMock.verify()
  490. })
  491. it('should create one folder', function () {
  492. expect(this.result.newFolders).to.have.length(1)
  493. })
  494. it('should return the new folder', function () {
  495. expect(this.result.folder.name).to.equal('new-folder')
  496. })
  497. it('should return the parent folder', function () {
  498. expect(this.result.folder.parentFolder_id).to.equal(this.folder._id)
  499. })
  500. })
  501. describe('when mutliple folders are missing', async function () {
  502. beforeEach(function () {
  503. this.folder1 = { _id: ObjectId(), name: 'folder1' }
  504. this.folder1Path = {
  505. fileSystem: '/test-folder/folder1',
  506. mongo: 'rootFolder.0.folders.0.folders.0',
  507. }
  508. this.folder2 = { _id: ObjectId(), name: 'folder2' }
  509. this.folder2Path = {
  510. fileSystem: '/test-folder/folder1/folder2',
  511. mongo: 'rootFolder.0.folders.0.folders.0.folders.0',
  512. }
  513. this.FolderModel.onFirstCall().returns(this.folder1)
  514. this.FolderModel.onSecondCall().returns(this.folder2)
  515. this.ProjectLocator.promises.findElement
  516. .withArgs({
  517. project: this.project,
  518. element_id: this.folder1._id,
  519. type: 'folder',
  520. })
  521. .resolves({
  522. element: this.folder1,
  523. path: this.folder1Path,
  524. })
  525. this.ProjectLocator.promises.findElement
  526. .withArgs({
  527. project: this.project,
  528. element_id: this.folder2._id,
  529. type: 'folder',
  530. })
  531. .resolves({
  532. element: this.folder2,
  533. path: this.folder2Path,
  534. })
  535. this.ProjectMock.expects('findOneAndUpdate')
  536. .withArgs(
  537. {
  538. _id: this.project._id,
  539. 'rootFolder.0.folders.0': { $exists: true },
  540. },
  541. {
  542. $push: {
  543. 'rootFolder.0.folders.0.folders': sinon.match({
  544. name: 'folder1',
  545. }),
  546. },
  547. $inc: { version: 1 },
  548. }
  549. )
  550. .chain('exec')
  551. .resolves(this.project)
  552. this.ProjectMock.expects('findOneAndUpdate')
  553. .withArgs(
  554. {
  555. _id: this.project._id,
  556. 'rootFolder.0.folders.0.folders.0': { $exists: true },
  557. },
  558. {
  559. $push: {
  560. 'rootFolder.0.folders.0.folders.0.folders': sinon.match({
  561. name: 'folder2',
  562. }),
  563. },
  564. $inc: { version: 1 },
  565. }
  566. )
  567. .chain('exec')
  568. .resolves(this.project)
  569. })
  570. ;[
  571. {
  572. description: 'without a trailing slash',
  573. path: '/test-folder/folder1/folder2',
  574. },
  575. {
  576. description: 'with a trailing slash',
  577. path: '/test-folder/folder1/folder2/',
  578. },
  579. ].forEach(({ description, path }) => {
  580. describe(description, function () {
  581. beforeEach(async function () {
  582. this.result = await this.subject.promises.mkdirp(
  583. this.project._id,
  584. path
  585. )
  586. })
  587. it('should update the database', function () {
  588. this.ProjectMock.verify()
  589. })
  590. it('should add multiple folders', function () {
  591. const newFolders = this.result.newFolders
  592. expect(newFolders).to.have.length(2)
  593. expect(newFolders[0].name).to.equal('folder1')
  594. expect(newFolders[1].name).to.equal('folder2')
  595. })
  596. it('should return the last folder', function () {
  597. expect(this.result.folder.name).to.equal('folder2')
  598. })
  599. it('should return the parent folder', function () {
  600. expect(this.result.folder.parentFolder_id).to.equal(
  601. this.folder1._id
  602. )
  603. })
  604. })
  605. })
  606. })
  607. })
  608. describe('moveEntity', function () {
  609. describe('moving a doc into a different folder', function () {
  610. beforeEach(async function () {
  611. this.pathAfterMove = {
  612. fileSystem: '/somewhere/else.txt',
  613. }
  614. this.oldDocs = ['old-doc']
  615. this.oldFiles = ['old-file']
  616. this.newDocs = ['new-doc']
  617. this.newFiles = ['new-file']
  618. this.ProjectEntityHandler.getAllEntitiesFromProject
  619. .onFirstCall()
  620. .returns({ docs: this.oldDocs, files: this.oldFiles })
  621. this.ProjectEntityHandler.getAllEntitiesFromProject
  622. .onSecondCall()
  623. .returns({ docs: this.newDocs, files: this.newFiles })
  624. this.ProjectMock.expects('findOneAndUpdate')
  625. .withArgs(
  626. {
  627. _id: this.project._id,
  628. 'rootFolder.0.folders.0': { $exists: true },
  629. },
  630. {
  631. $push: { 'rootFolder.0.folders.0.docs': this.doc },
  632. $inc: { version: 1 },
  633. }
  634. )
  635. .chain('exec')
  636. .resolves(this.project)
  637. this.ProjectMock.expects('findOneAndUpdate')
  638. .withArgs(
  639. { _id: this.project._id },
  640. {
  641. $pull: { 'rootFolder.0.docs': { _id: this.doc._id } },
  642. $inc: { version: 1 },
  643. }
  644. )
  645. .chain('exec')
  646. .resolves(this.project)
  647. this.result = await this.subject.promises.moveEntity(
  648. this.project._id,
  649. this.doc._id,
  650. this.folder._id,
  651. 'doc'
  652. )
  653. })
  654. it('should update the database', function () {
  655. this.ProjectMock.verify()
  656. })
  657. it('should report what changed', function () {
  658. expect(this.result).to.deep.equal({
  659. project: this.project,
  660. startPath: '/test-doc.txt',
  661. endPath: '/test-folder/test-doc.txt',
  662. rev: this.doc.rev,
  663. changes: {
  664. oldDocs: this.oldDocs,
  665. newDocs: this.newDocs,
  666. oldFiles: this.oldFiles,
  667. newFiles: this.newFiles,
  668. newProject: this.project,
  669. },
  670. })
  671. })
  672. })
  673. describe('when moving a folder inside itself', function () {
  674. it('throws an error', async function () {
  675. await expect(
  676. this.subject.promises.moveEntity(
  677. this.project._id,
  678. this.folder._id,
  679. this.folder._id,
  680. 'folder'
  681. )
  682. ).to.be.rejectedWith(Errors.InvalidNameError)
  683. })
  684. })
  685. describe('when moving a folder to a subfolder of itself', function () {
  686. it('throws an error', async function () {
  687. await expect(
  688. this.subject.promises.moveEntity(
  689. this.project._id,
  690. this.folder._id,
  691. this.subfolder._id,
  692. 'folder'
  693. )
  694. ).to.be.rejectedWith(Errors.InvalidNameError)
  695. })
  696. })
  697. })
  698. describe('deleteEntity', function () {
  699. beforeEach(async function () {
  700. this.ProjectMock.expects('findOneAndUpdate')
  701. .withArgs(
  702. { _id: this.project._id },
  703. {
  704. $pull: { 'rootFolder.0.docs': { _id: this.doc._id } },
  705. $inc: { version: 1 },
  706. }
  707. )
  708. .chain('exec')
  709. .resolves(this.project)
  710. await this.subject.promises.deleteEntity(
  711. this.project._id,
  712. this.doc._id,
  713. 'doc'
  714. )
  715. })
  716. it('should update the database', function () {
  717. this.ProjectMock.verify()
  718. })
  719. })
  720. describe('renameEntity', function () {
  721. describe('happy path', function () {
  722. beforeEach(async function () {
  723. this.newName = 'new.tex'
  724. this.oldDocs = ['old-doc']
  725. this.oldFiles = ['old-file']
  726. this.newDocs = ['new-doc']
  727. this.newFiles = ['new-file']
  728. this.ProjectEntityHandler.getAllEntitiesFromProject
  729. .onFirstCall()
  730. .returns({ docs: this.oldDocs, files: this.oldFiles })
  731. this.ProjectEntityHandler.getAllEntitiesFromProject
  732. .onSecondCall()
  733. .returns({ docs: this.newDocs, files: this.newFiles })
  734. this.ProjectMock.expects('findOneAndUpdate')
  735. .withArgs(
  736. { _id: this.project._id, 'rootFolder.0.docs.0': { $exists: true } },
  737. {
  738. $set: { 'rootFolder.0.docs.0.name': this.newName },
  739. $inc: { version: 1 },
  740. }
  741. )
  742. .chain('exec')
  743. .resolves(this.project)
  744. this.result = await this.subject.promises.renameEntity(
  745. this.project._id,
  746. this.doc._id,
  747. 'doc',
  748. this.newName
  749. )
  750. })
  751. it('should update the database', function () {
  752. this.ProjectMock.verify()
  753. })
  754. it('returns info', function () {
  755. expect(this.result).to.deep.equal({
  756. project: this.project,
  757. startPath: '/test-doc.txt',
  758. endPath: '/new.tex',
  759. rev: this.doc.rev,
  760. changes: {
  761. oldDocs: this.oldDocs,
  762. newDocs: this.newDocs,
  763. oldFiles: this.oldFiles,
  764. newFiles: this.newFiles,
  765. newProject: this.project,
  766. },
  767. })
  768. })
  769. })
  770. describe('name already exists', function () {
  771. it('should throw an error', async function () {
  772. await expect(
  773. this.subject.promises.renameEntity(
  774. this.project._id,
  775. this.doc._id,
  776. 'doc',
  777. this.folder.name
  778. )
  779. ).to.be.rejectedWith(Errors.InvalidNameError)
  780. })
  781. })
  782. })
  783. describe('_putElement', function () {
  784. describe('updating the project', function () {
  785. describe('when the parent folder is given', function () {
  786. beforeEach(function () {
  787. this.newFile = { _id: ObjectId(), name: 'new file.png' }
  788. this.ProjectMock.expects('findOneAndUpdate')
  789. .withArgs(
  790. {
  791. _id: this.project._id,
  792. 'rootFolder.0.folders.0': { $exists: true },
  793. },
  794. {
  795. $push: { 'rootFolder.0.folders.0.fileRefs': this.newFile },
  796. $inc: { version: 1 },
  797. }
  798. )
  799. .chain('exec')
  800. .resolves(this.project)
  801. })
  802. it('should update the database', async function () {
  803. await this.subject.promises._putElement(
  804. this.project,
  805. this.folder._id,
  806. this.newFile,
  807. 'files'
  808. )
  809. this.ProjectMock.verify()
  810. })
  811. it('should add an s onto the type if not included', async function () {
  812. await this.subject.promises._putElement(
  813. this.project,
  814. this.folder._id,
  815. this.newFile,
  816. 'file'
  817. )
  818. this.ProjectMock.verify()
  819. })
  820. })
  821. describe('error cases', function () {
  822. it('should throw an error if element is null', async function () {
  823. await expect(
  824. this.subject.promises._putElement(
  825. this.project,
  826. this.folder._id,
  827. null,
  828. 'file'
  829. )
  830. ).to.be.rejected
  831. })
  832. it('should error if the element has no _id', async function () {
  833. const file = { name: 'something' }
  834. await expect(
  835. this.subject.promises._putElement(
  836. this.project,
  837. this.folder._id,
  838. file,
  839. 'file'
  840. )
  841. ).to.be.rejected
  842. })
  843. it('should error if element name contains invalid characters', async function () {
  844. const file = { _id: ObjectId(), name: 'something*bad' }
  845. await expect(
  846. this.subject.promises._putElement(
  847. this.project,
  848. this.folder._id,
  849. file,
  850. 'file'
  851. )
  852. ).to.be.rejected
  853. })
  854. it('should error if element name is too long', async function () {
  855. const file = {
  856. _id: ObjectId(),
  857. name: 'long-'.repeat(1000) + 'something',
  858. }
  859. await expect(
  860. this.subject.promises._putElement(
  861. this.project,
  862. this.folder._id,
  863. file,
  864. 'file'
  865. )
  866. ).to.be.rejectedWith(Errors.InvalidNameError)
  867. })
  868. it('should error if the folder name is too long', async function () {
  869. const file = {
  870. _id: ObjectId(),
  871. name: 'something',
  872. }
  873. this.ProjectLocator.promises.findElement
  874. .withArgs({
  875. project: this.project,
  876. element_id: this.folder._id,
  877. type: 'folder',
  878. })
  879. .resolves({
  880. element: this.folder,
  881. path: { fileSystem: 'subdir/'.repeat(1000) + 'foo' },
  882. })
  883. await expect(
  884. this.subject.promises._putElement(
  885. this.project,
  886. this.folder._id,
  887. file,
  888. 'file'
  889. )
  890. ).to.be.rejectedWith(Errors.InvalidNameError)
  891. })
  892. ;['file', 'doc', 'folder'].forEach(entityType => {
  893. it(`should error if a ${entityType} already exists with the same name`, async function () {
  894. const file = {
  895. _id: ObjectId(),
  896. name: this[entityType].name,
  897. }
  898. await expect(
  899. this.subject.promises._putElement(
  900. this.project,
  901. null,
  902. file,
  903. 'file'
  904. )
  905. ).to.be.rejectedWith(Errors.InvalidNameError)
  906. })
  907. })
  908. })
  909. })
  910. describe('when the parent folder is not given', function () {
  911. it('should default to root folder insert', async function () {
  912. this.newFile = { _id: ObjectId(), name: 'new file.png' }
  913. this.ProjectMock.expects('findOneAndUpdate')
  914. .withArgs(
  915. { _id: this.project._id, 'rootFolder.0': { $exists: true } },
  916. {
  917. $push: { 'rootFolder.0.fileRefs': this.newFile },
  918. $inc: { version: 1 },
  919. }
  920. )
  921. .chain('exec')
  922. .resolves(this.project)
  923. await this.subject.promises._putElement(
  924. this.project,
  925. this.rootFolder._id,
  926. this.newFile,
  927. 'file'
  928. )
  929. })
  930. })
  931. })
  932. describe('_insertDeletedFileReference', function () {
  933. beforeEach(async function () {
  934. this.DeletedFileMock.expects('create')
  935. .withArgs({
  936. projectId: this.project._id,
  937. _id: this.file._id,
  938. name: this.file.name,
  939. linkedFileData: this.file.linkedFileData,
  940. hash: this.file.hash,
  941. deletedAt: sinon.match.date,
  942. })
  943. .resolves()
  944. await this.subject.promises._insertDeletedFileReference(
  945. this.project._id,
  946. this.file
  947. )
  948. })
  949. it('should update the database', function () {
  950. this.DeletedFileMock.verify()
  951. })
  952. })
  953. describe('createNewFolderStructure', function () {
  954. beforeEach(function () {
  955. this.mockRootFolder = 'MOCK_ROOT_FOLDER'
  956. this.docUploads = ['MOCK_DOC_UPLOAD']
  957. this.fileUploads = ['MOCK_FILE_UPLOAD']
  958. this.FolderStructureBuilder.buildFolderStructure
  959. .withArgs(this.docUploads, this.fileUploads)
  960. .returns(this.mockRootFolder)
  961. this.updateExpectation = this.ProjectMock.expects('findOneAndUpdate')
  962. .withArgs(
  963. {
  964. _id: this.project._id,
  965. 'rootFolder.0.folders.0': { $exists: false },
  966. 'rootFolder.0.docs.0': { $exists: false },
  967. 'rootFolder.0.files.0': { $exists: false },
  968. },
  969. { $set: { rootFolder: [this.mockRootFolder] }, $inc: { version: 1 } },
  970. { new: true, lean: true, fields: { version: 1 } }
  971. )
  972. .chain('exec')
  973. })
  974. describe('happy path', function () {
  975. beforeEach(async function () {
  976. this.updateExpectation.resolves({ version: 1 })
  977. await this.subject.promises.createNewFolderStructure(
  978. this.project._id,
  979. this.docUploads,
  980. this.fileUploads
  981. )
  982. })
  983. it('updates the database', function () {
  984. this.ProjectMock.verify()
  985. })
  986. })
  987. describe("when the update doesn't find a matching document", function () {
  988. beforeEach(async function () {
  989. this.updateExpectation.resolves(null)
  990. })
  991. it('throws an error', async function () {
  992. await expect(
  993. this.subject.promises.createNewFolderStructure(
  994. this.project._id,
  995. this.docUploads,
  996. this.fileUploads
  997. )
  998. ).to.be.rejected
  999. })
  1000. })
  1001. })
  1002. describe('replaceDocWithFile', function () {
  1003. it('should simultaneously remove the doc and add the file', async function () {
  1004. this.ProjectMock.expects('findOneAndUpdate')
  1005. .withArgs(
  1006. { _id: this.project._id, 'rootFolder.0': { $exists: true } },
  1007. {
  1008. $pull: { 'rootFolder.0.docs': { _id: this.doc._id } },
  1009. $push: { 'rootFolder.0.fileRefs': this.file },
  1010. $inc: { version: 1 },
  1011. },
  1012. { new: true }
  1013. )
  1014. .chain('exec')
  1015. .resolves(this.project)
  1016. await this.subject.promises.replaceDocWithFile(
  1017. this.project._id,
  1018. this.doc._id,
  1019. this.file
  1020. )
  1021. this.ProjectMock.verify()
  1022. })
  1023. })
  1024. describe('replaceFileWithDoc', function () {
  1025. it('should simultaneously remove the file and add the doc', async function () {
  1026. this.ProjectMock.expects('findOneAndUpdate')
  1027. .withArgs(
  1028. { _id: this.project._id, 'rootFolder.0': { $exists: true } },
  1029. {
  1030. $pull: { 'rootFolder.0.fileRefs': { _id: this.file._id } },
  1031. $push: { 'rootFolder.0.docs': this.doc },
  1032. $inc: { version: 1 },
  1033. },
  1034. { new: true }
  1035. )
  1036. .chain('exec')
  1037. .resolves(this.project)
  1038. await this.subject.promises.replaceFileWithDoc(
  1039. this.project._id,
  1040. this.file._id,
  1041. this.doc
  1042. )
  1043. this.ProjectMock.verify()
  1044. })
  1045. })
  1046. })