ProjectEntityMongoUpdateHandlerTests.js 32 KB

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