ProjectEntityMongoUpdateHandlerTests.js 32 KB

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