ProjectEntityMongoUpdateHandlerTests.js 31 KB

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