ProjectEntityMongoUpdateHandlerTests.js 33 KB

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