ProjectEntityMongoUpdateHandlerTests.js 35 KB

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